diff --git a/adapters/rhythmone/params_test.go b/adapters/rhythmone/params_test.go new file mode 100644 index 00000000000..7d8cad47d53 --- /dev/null +++ b/adapters/rhythmone/params_test.go @@ -0,0 +1,57 @@ +package rhythmone + +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-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderRhythmone, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected rhythmone params: %s", validParam) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderRhythmone, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId":"123", "zone":"12345", "path":"34567"}`, +} + +var invalidParams = []string{ + `{"placementId":"123", "zone":"12345", "path":34567}`, + `{"placementId":"123", "zone":12345, "path":"34567"}`, + `{"placementId":123, "zone":"12345", "path":"34567"}`, + `{"placementId":123, "zone":12345, "path":34567}`, + `{"placementId":123, "zone":12345, "path":"34567"}`, + `{"appId":"123", "bidfloor":0.01}`, + `{"publisherName": 100}`, + `{"placementId": 1234}`, + `{"zone": true}`, + ``, + `null`, + `nil`, + `true`, + `9`, + `[]`, + `{}`, +} diff --git a/adapters/rhythmone/rhythmone.go b/adapters/rhythmone/rhythmone.go new file mode 100644 index 00000000000..b630d9e933f --- /dev/null +++ b/adapters/rhythmone/rhythmone.go @@ -0,0 +1,146 @@ +package rhythmone + +import ( + "encoding/json" + "fmt" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type RhythmoneAdapter struct { + endPoint string +} + +func (a *RhythmoneAdapter) MakeRequests(request *openrtb.BidRequest) ([]*adapters.RequestData, []error) { + errs := make([]error, 0, len(request.Imp)) + + var uri string + request, uri, errs = a.preProcess(request, errs) + if request != nil { + reqJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + if uri != "" { + 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: reqJSON, + Headers: headers, + }}, errs + } + } + return nil, errs +} + +func (a *RhythmoneAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("bad server response: %d. ", err), + }} + } + + var errs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp), + }) + } + } + return bidResponse, errs +} + +func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impId { + if imp.Banner != nil { + mediaType = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + return mediaType + } + } + return mediaType +} + +func NewRhythmoneBidder(endpoint string) *RhythmoneAdapter { + return &RhythmoneAdapter{ + endPoint: endpoint, + } +} + +func (a *RhythmoneAdapter) preProcess(req *openrtb.BidRequest, errors []error) (*openrtb.BidRequest, string, []error) { + numRequests := len(req.Imp) + var uri string = "" + for i := 0; i < numRequests; i++ { + imp := req.Imp[i] + var bidderExt adapters.ExtImpBidder + err := json.Unmarshal(imp.Ext, &bidderExt) + if err != nil { + err = &errortypes.BadInput{ + Message: fmt.Sprintf("ext data not provided in imp id=%s. Abort all Request", imp.ID), + } + errors = append(errors, err) + return nil, "", errors + } + var rhythmoneExt openrtb_ext.ExtImpRhythmone + err = json.Unmarshal(bidderExt.Bidder, &rhythmoneExt) + if err != nil { + err = &errortypes.BadInput{ + Message: fmt.Sprintf("placementId | zone | path not provided in imp id=%s. Abort all Request", imp.ID), + } + errors = append(errors, err) + return nil, "", errors + } + rhythmoneExt.S2S = true + rhythmoneExtCopy, err := json.Marshal(&rhythmoneExt) + if err != nil { + errors = append(errors, err) + return nil, "", errors + } + bidderExtCopy := openrtb_ext.ExtBid{ + Bidder: rhythmoneExtCopy, + } + impExtCopy, err := json.Marshal(&bidderExtCopy) + if err != nil { + errors = append(errors, err) + return nil, "", errors + } + imp.Ext = impExtCopy + req.Imp[i] = imp + if uri == "" { + uri = fmt.Sprintf("%s/%s/0/%s?z=%s&s2s=%s", a.endPoint, rhythmoneExt.PlacementId, rhythmoneExt.Path, rhythmoneExt.Zone, "true") + } + } + return req, uri, errors +} diff --git a/adapters/rhythmone/rhythmone_test.go b/adapters/rhythmone/rhythmone_test.go new file mode 100644 index 00000000000..9a54623dbe9 --- /dev/null +++ b/adapters/rhythmone/rhythmone_test.go @@ -0,0 +1,10 @@ +package rhythmone + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "rhythmonetest", NewRhythmoneBidder("http://tag.1rx.io/rmp")) +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-app.json b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-app.json new file mode 100644 index 00000000000..ea23d3b4ec4 --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-app.json @@ -0,0 +1,201 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "app": { + "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + "name": "Yahoo Weather", + "bundle": "12345", + "storeurl": "https://itunes.apple.com/id628677149", + "cat": ["IAB15", "IAB15-10"], + "ver": "1.0.2", + "publisher": { + "id": "1" + } + }, + "device": + { + "dnt": 0, + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit / 534.46(KHTML, like Gecko) Version / 5.1 Mobile / 9 A334 Safari / 7534.48 .3", + "ip": "123.145.167.189", + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "carrier": "VERIZON", + "language": "en", + "make": "Apple", + "model": "iPhone", + "os": "iOS", + "osv": "6.1", + "js": 1, + "connectiontype": 3, + "devicetype": 1 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 300, + "h": 300 + }] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }, { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }], + "app": { + "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + "name": "Yahoo Weather", + "bundle": "12345", + "storeurl": "https://itunes.apple.com/id628677149", + "cat": ["IAB15", "IAB15-10"], + "ver": "1.0.2", + "publisher": { + "id": "1" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit / 534.46(KHTML, like Gecko) Version / 5.1 Mobile / 9 A334 Safari / 7534.48 .3", + "ip": "123.145.167.189", + "devicetype": 1, + "make": "Apple", + "model": "iPhone", + "os": "iOS", + "osv": "6.1", + "js": 1, + "language": "en", + "carrier": "VERIZON", + "connectiontype": 3, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-gdpr.json b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-gdpr.json new file mode 100644 index 00000000000..61481a3155f --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-gdpr.json @@ -0,0 +1,174 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "user": + { + "id": "eyJ0ZW1wVUlEcyI6eyJhZGZvcm0iOnsidWlkIjoiMzA5MTMwOTUxNjQ5NDA1MjcxIiwiZXhwaXJlcyI6IjIwMTgtMDYtMjBUMTE6NDA6MzUuODAwNTE0NzQ3KzA1OjMwIn0sImFkbnhzIjp7InVpZCI6IjM1MTUzMjg2MTAyNjMxNjQ0ODQiLCJleHBpcmVzIjoiMjAxOC0wNi0xOFQxODoxMjoxNy4wMTExMzg2MDgrMDU6MzAifX0sImJkYXkiOiIyMDE4LTA2LTA0VDE4OjEyOjE3LjAxMTEzMDg3NSswNTozMCJ9", + "ext": { + "consent": "BOPVK28OPVK28ABABAENA8-AAAADkCNQCGoQAAQ" + } + }, + "regs": + { + "ext": { + "gdpr": 1 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body": +{ + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 300, + "h": 300 + }] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }, { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }], + "user": { + "id": "eyJ0ZW1wVUlEcyI6eyJhZGZvcm0iOnsidWlkIjoiMzA5MTMwOTUxNjQ5NDA1MjcxIiwiZXhwaXJlcyI6IjIwMTgtMDYtMjBUMTE6NDA6MzUuODAwNTE0NzQ3KzA1OjMwIn0sImFkbnhzIjp7InVpZCI6IjM1MTUzMjg2MTAyNjMxNjQ0ODQiLCJleHBpcmVzIjoiMjAxOC0wNi0xOFQxODoxMjoxNy4wMTExMzg2MDgrMDU6MzAifX0sImJkYXkiOiIyMDE4LTA2LTA0VDE4OjEyOjE3LjAxMTEzMDg3NSswNTozMCJ9", + "ext": { + "consent": "BOPVK28OPVK28ABABAENA8-AAAADkCNQCGoQAAQ" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-site.json b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-site.json new file mode 100644 index 00000000000..7a32f2732c8 --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video-site.json @@ -0,0 +1,181 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ], + "site": { + "id": "102855", + "cat": ["IAB3-1"], + "domain": "www.foobar.com", + "page": "http://www.foobar.com/1234.html ", + "publisher": { + "id": "8953", + "name": "foobar.com", + "cat": ["IAB3-1"], + "domain": "foobar.com" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version / 5.1 .7 Safari / 534.57 .2", + "ip": "123.145.167.10" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 300, + "h": 300 + }] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }, { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }], + "site": { + "id": "102855", + "cat": ["IAB3-1"], + "domain": "www.foobar.com", + "page": "http://www.foobar.com/1234.html ", + "publisher": { + "id": "8953", + "name": "foobar.com", + "cat": ["IAB3-1"], + "domain": "foobar.com" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version / 5.1 .7 Safari / 534.57 .2", + "ip": "123.145.167.10" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video.json b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video.json new file mode 100644 index 00000000000..1de5cc1c7af --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/banner-and-video.json @@ -0,0 +1,149 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + }, + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body":{ + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 300, + "h": 300 + }] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }, { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 576, + "w": 1024 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/simple-banner.json b/adapters/rhythmone/rhythmonetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..9d5a9dcbcc6 --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/simple-banner.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body":{ + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 300, + "h": 300 + }] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["yahoo.com"], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] +} diff --git a/adapters/rhythmone/rhythmonetest/exemplary/simple-video.json b/adapters/rhythmone/rhythmonetest/exemplary/simple-video.json new file mode 100644 index 00000000000..5c22c9a8203 --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/exemplary/simple-video.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "path": "mvo", + "zone": "1r" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://tag.1rx.io/rmp/72721/0/mvo?z=1r&s2s=true", + "body":{ + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + }, + "ext": { + "bidder": { + "placementId": "72721", + "zone": "1r", + "path": "mvo", + "S2S": true + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "Rhythmone", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + }] + } + ] + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] +} diff --git a/adapters/rhythmone/rhythmonetest/supplemental/missing-extension.json b/adapters/rhythmone/rhythmonetest/supplemental/missing-extension.json new file mode 100644 index 00000000000..7541f003078 --- /dev/null +++ b/adapters/rhythmone/rhythmonetest/supplemental/missing-extension.json @@ -0,0 +1,29 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-ext-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 2, + "maxextended": 30, + "minbitrate": 300, + "maxbitrate": 1500, + "protocols": [1, 2, 5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1, 2, 3, 4] + } + } + ] + }, + "expectedMakeRequestsErrors": [ + "ext data not provided in imp id=test-missing-ext-id. Abort all Request" +] +} diff --git a/config/config.go b/config/config.go index 562b70af60d..7116e281331 100644 --- a/config/config.go +++ b/config/config.go @@ -361,6 +361,8 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.sovrn.usersync_url", "//ap.lijit.com/pixel?") v.SetDefault("adapters.adkerneladn.usersync_url", "https://tag.adkernel.com/syncr?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r=") v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}") + v.SetDefault("adapters.rhythmone.endpoint", "http://tag.1rx.io/rmp") + v.SetDefault("adapters.rhythmone.usersync_url", "//sync.1rx.io/usersync2/rmphb?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redir=") v.SetDefault("max_request_size", 1024*256) v.SetDefault("analytics.file.filename", "") diff --git a/config/config_test.go b/config/config_test.go index cef1d6a8090..1d7500895bf 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -159,6 +159,8 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "adapters.brightroll.endpoint", cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, "http://east-bid.ybp.yahoo.com/bid/appnexuspbs") cmpStrings(t, "adapters.brightroll.usersync_url", cfg.Adapters[string(openrtb_ext.BidderBrightroll)].UserSyncURL, "http://east-bid.ybp.yahoo.com/sync/appnexuspbs?gdpr={{gdpr}}&euconsent={{gdpr_consent}}&url=%s") cmpStrings(t, "adapters.adkerneladn.usersync_url", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernelAdn))].UserSyncURL, "https://tag.adkernel.com/syncr?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r=") + cmpStrings(t, "adapters.rhythmone.endpoint", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint, "http://tag.1rx.io/rmp") + cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "//sync.1rx.io/usersync2/rmphb?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redir=") } func TestValidConfig(t *testing.T) { diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 93be7f15b84..5c8351c8ab3 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -19,6 +19,7 @@ import ( "github.com/prebid/prebid-server/adapters/openx" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pulsepoint" + "github.com/prebid/prebid-server/adapters/rhythmone" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/somoaudience" "github.com/prebid/prebid-server/adapters/sovrn" @@ -41,6 +42,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderEPlanning: eplanning.NewEPlanningBidder(client, cfg.Adapters[string(openrtb_ext.BidderEPlanning)].Endpoint), openrtb_ext.BidderOpenx: openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint), openrtb_ext.BidderPubmatic: pubmatic.NewPubmaticBidder(client, cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint), + openrtb_ext.BidderRhythmone: rhythmone.NewRhythmoneBidder(cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint), openrtb_ext.BidderRubicon: rubicon.NewRubiconBidder( client, cfg.Adapters[string(openrtb_ext.BidderRubicon)].Endpoint, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index b345cc1c661..d8e0af5a70e 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -34,6 +34,7 @@ const ( BidderOpenx BidderName = "openx" BidderPubmatic BidderName = "pubmatic" BidderPulsepoint BidderName = "pulsepoint" + BidderRhythmone BidderName = "rhythmone" BidderRubicon BidderName = "rubicon" BidderSomoaudience BidderName = "somoaudience" BidderSovrn BidderName = "sovrn" @@ -55,6 +56,7 @@ var BidderMap = map[string]BidderName{ "openx": BidderOpenx, "pubmatic": BidderPubmatic, "pulsepoint": BidderPulsepoint, + "rhythmone": BidderRhythmone, "rubicon": BidderRubicon, "somoaudience": BidderSomoaudience, "sovrn": BidderSovrn, diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index bf4a8cf7f7a..7e1f73ea9b6 100644 --- a/openrtb_ext/imp.go +++ b/openrtb_ext/imp.go @@ -2,10 +2,11 @@ package openrtb_ext // ExtImp defines the contract for bidrequest.imp[i].ext type ExtImp struct { - Prebid *ExtImpPrebid `json:"prebid"` - Appnexus *ExtImpAppnexus `json:"appnexus"` - Rubicon *ExtImpRubicon `json:"rubicon"` - Adform *ExtImpAdform `json:"adform"` + Prebid *ExtImpPrebid `json:"prebid"` + Appnexus *ExtImpAppnexus `json:"appnexus"` + Rubicon *ExtImpRubicon `json:"rubicon"` + Adform *ExtImpAdform `json:"adform"` + Rhythmone *ExtImpRhythmone `json:"rhythmone"` } // ExtImpPrebid defines the contract for bidrequest.imp[i].ext.prebid diff --git a/openrtb_ext/imp_rhythmone.go b/openrtb_ext/imp_rhythmone.go new file mode 100644 index 00000000000..fb5dc199c77 --- /dev/null +++ b/openrtb_ext/imp_rhythmone.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpRhythmone defines the contract for bidrequest.imp[i].ext.rhythmone +type ExtImpRhythmone struct { + PlacementId string `json:"placementId"` + Zone string `json:"zone"` + Path string `json:"path"` + S2S bool +} diff --git a/static/bidder-info/rhythmone.yaml b/static/bidder-info/rhythmone.yaml new file mode 100644 index 00000000000..89da6cfea35 --- /dev/null +++ b/static/bidder-info/rhythmone.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@rhythmone.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/rhythmone.json b/static/bidder-params/rhythmone.json new file mode 100644 index 00000000000..01366b45607 --- /dev/null +++ b/static/bidder-params/rhythmone.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rhythmone Adapter Params", + "description": "A schema which validates params accepted by the Rhythmone adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "An ID which is used to frame Rhythmone ad tag", + "minLength": 1 + }, + "path": { + "type": "string", + "description": "An ID which is used to frame Rhythmone ad tag", + "minLength": 1 + }, + "zone": { + "type": "string", + "description": "An ID which is used to frame Rhythmone ad tag", + "minLength": 1 + } + }, + "required": ["placementId", "path", "zone"] +} diff --git a/usersync/usersyncers/rhythmone.go b/usersync/usersyncers/rhythmone.go new file mode 100644 index 00000000000..6b4130bc4e4 --- /dev/null +++ b/usersync/usersyncers/rhythmone.go @@ -0,0 +1,18 @@ +package usersyncers + +import ( + "net/url" + "strings" +) + +func NewRhythmoneSyncer(usersyncURL string, externalURL string) *syncer { + externalURL = strings.TrimRight(externalURL, "/") + redirectURI := url.QueryEscape(externalURL) + "%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{gdpr}}%26gdpr_consent%3D{{gdpr_consent}}%26uid%3D%5BRX_UUID%5D" + + return &syncer{ + familyName: "rhythmone", + gdprVendorID: 36, + syncEndpointBuilder: resolveMacros(usersyncURL + redirectURI), + syncType: SyncTypeRedirect, + } +} diff --git a/usersync/usersyncers/rhythmone_test.go b/usersync/usersyncers/rhythmone_test.go new file mode 100644 index 00000000000..616d7621ffb --- /dev/null +++ b/usersync/usersyncers/rhythmone_test.go @@ -0,0 +1,21 @@ +package usersyncers + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRhythmoneSyncer(t *testing.T) { + assert := assert.New(t) + an := NewRhythmoneSyncer("https://sync.1rx.io/usersync2/rmphb?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redir=", "localhost") + syncInfo := an.GetUsersyncInfo("1", "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA") + url := "https://sync.1rx.io/usersync2/rmphb?gdpr=1&gdpr_consent=BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA&redir=localhost%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D1%26gdpr_consent%3DBOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA%26uid%3D%5BRX_UUID%5D" + assert.Equal(url, syncInfo.URL) + assert.Equal("redirect", syncInfo.Type) + if syncInfo.SupportCORS != false { + t.Errorf("should have been false") + } + if an.GDPRVendorID() != 36 { + t.Errorf("Wrong Rhythmone GDPR VendorID. Got %d", an.GDPRVendorID()) + } +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index a4612f1f04b..30f80e599f9 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -26,6 +26,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync openrtb_ext.BidderOpenx: NewOpenxSyncer(cfg.ExternalURL), openrtb_ext.BidderPubmatic: NewPubmaticSyncer(cfg.ExternalURL), openrtb_ext.BidderPulsepoint: NewPulsepointSyncer(cfg.ExternalURL), + openrtb_ext.BidderRhythmone: NewRhythmoneSyncer(cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, cfg.ExternalURL), openrtb_ext.BidderRubicon: NewRubiconSyncer(cfg.Adapters[string(openrtb_ext.BidderRubicon)].UserSyncURL), openrtb_ext.BidderSomoaudience: NewSomoaudienceSyncer(cfg.ExternalURL), openrtb_ext.BidderSovrn: NewSovrnSyncer(cfg.ExternalURL, cfg.Adapters[string(openrtb_ext.BidderSovrn)].UserSyncURL),