diff --git a/adapters/targetVideo/params_test.go b/adapters/targetVideo/params_test.go new file mode 100644 index 00000000000..185b1bb75bb --- /dev/null +++ b/adapters/targetVideo/params_test.go @@ -0,0 +1,49 @@ +package targetVideo + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v3/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.BidderTargetVideo, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected targetVideo 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.BidderTargetVideo, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId":846}`, + `{"placementId":"846"}`, +} + +var invalidParams = []string{ + `null`, + `nil`, + `undefined`, + `{"placementId": "%9"}`, + `{"publisherId": "as9""}`, + `{"placementId": true}`, + `{"placementId": ""}`, +} diff --git a/adapters/targetVideo/targetvideo.go b/adapters/targetVideo/targetvideo.go new file mode 100644 index 00000000000..5101672277a --- /dev/null +++ b/adapters/targetVideo/targetvideo.go @@ -0,0 +1,140 @@ +package targetVideo + +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 +} + +type impExtPrebid struct { + Prebid *openrtb_ext.ExtImpPrebid `json:"prebid,omitempty"` +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + totalImps := len(request.Imp) + errors := make([]error, 0) + adapterRequests := make([]*adapters.RequestData, 0, totalImps) + + // Split multi-imp request into multiple ad server requests. SRA is currently not recommended. + for i := 0; i < totalImps; i++ { + adapterReq, err := a.makeRequest(*request, request.Imp[i]) + if err != nil { + errors = append(errors, err) + continue + } + adapterRequests = append(adapterRequests, adapterReq) + } + + return adapterRequests, errors +} + +func (a *adapter) makeRequest(request openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) { + + // For now, this adapter sends one imp per request, but we still + // iterate over all imps in the request to perform the required + // imp.ext transformation. + request.Imp = []openrtb2.Imp{imp} + + for i := range request.Imp { + + var extBidder adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &extBidder); err != nil { + return nil, &errortypes.BadInput{Message: fmt.Sprintf("Invalid ext.bidder")} + } + var extImpTargetVideo openrtb_ext.ExtImpTargetVideo + if err := jsonutil.Unmarshal(extBidder.Bidder, &extImpTargetVideo); err != nil { + return nil, &errortypes.BadInput{Message: fmt.Sprintf("Placement ID missing")} + } + var prebid *openrtb_ext.ExtImpPrebid + if extBidder.Prebid == nil { + prebid = &openrtb_ext.ExtImpPrebid{} + } + if prebid.StoredRequest == nil { + prebid.StoredRequest = &openrtb_ext.ExtStoredRequest{} + } + prebid.StoredRequest.ID = fmt.Sprintf("%d", extImpTargetVideo.PlacementId) + + ext := impExtPrebid{ + Prebid: prebid, + } + + extRaw, err := jsonutil.Marshal(ext) + if err != nil { + return nil, &errortypes.BadInput{Message: fmt.Sprintf("error building imp.ext, err: %s", err)} + } + + request.Imp[i].Ext = extRaw + + } + + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + //fmt.Println("TARGET VIDEO reqJson: ", string(reqJSON)) + + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +func (a *adapter) MakeBids(bidReq *openrtb2.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(httpRes) { + return nil, nil + } + if statusError := adapters.CheckResponseStatusCodeForErrors(httpRes); statusError != nil { + return nil, []error{statusError} + } + + bidResp, errResp := prepareBidResponse(httpRes.Body) + if errResp != nil { + return nil, []error{errResp} + } + + br := adapters.NewBidderResponse() + errs := []error{} + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bid := sb.Bid[i] + br.Bids = append(br.Bids, &adapters.TypedBid{Bid: &bid, BidType: openrtb_ext.BidTypeVideo}) + br.Currency = bidResp.Cur + } + } + return br, errs +} + +func prepareBidResponse(body []byte) (openrtb2.BidResponse, error) { + var response openrtb2.BidResponse + if err := jsonutil.Unmarshal(body, &response); err != nil { + return response, err + } + return response, nil +} + +func Builder(bidderName openrtb_ext.BidderName, cfg config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: cfg.Endpoint, + } + return bidder, nil +} diff --git a/adapters/targetVideo/targetvideo_test.go b/adapters/targetVideo/targetvideo_test.go new file mode 100644 index 00000000000..c25a50014be --- /dev/null +++ b/adapters/targetVideo/targetvideo_test.go @@ -0,0 +1,20 @@ +package targetVideo + +import ( + "testing" + + "github.com/prebid/prebid-server/v3/adapters/adapterstest" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderTargetVideo, config.Adapter{ + Endpoint: "http://localhost/pbs"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "targetvideotest", bidder) +} diff --git a/adapters/targetVideo/targetvideotest/exemplary/app-video.json b/adapters/targetVideo/targetvideotest/exemplary/app-video.json new file mode 100644 index 00000000000..88557a1e46b --- /dev/null +++ b/adapters/targetVideo/targetvideotest/exemplary/app-video.json @@ -0,0 +1,116 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "app": { + "id": "appID", + "publisher": { + "id": "uniq_pub_id" + } + }, + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "77777" + } + } + } + ], + "device": { + "ua": "test-user-agent" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"] + }, + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id-video", + "app": { + "id": "appID", + "publisher": { + "id": "uniq_pub_id" + } + }, + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "prebid": { + "storedrequest": { + "id": "77777" + } + } + } + } + ], + "device": { + "ua": "test-user-agent" + }, + "cur": ["EUR"] + }, + "impIDs":["test-imp-id-video"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-video", + "seatbid": [ + { + "seat": "targetVideo", + "bid": [ + { + "id": "randomID", + "impid": "test-imp-id-video", + "price": 5.5, + "adid": "12345678", + "adm": "test-imp-id-video", + "cid": "789", + "crid": "12345678", + "h": 360, + "w": 640 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids" : [{ + "bid": { + "id": "randomID", + "adid": "12345678", + "impid": "test-imp-id-video", + "price": 5.5, + "adm": "test-imp-id-video", + "crid": "12345678", + "cid": "789", + "h": 360, + "w": 640 + }, + "type": "video" + }] + } + ] +} diff --git a/adapters/targetVideo/targetvideotest/exemplary/video.json b/adapters/targetVideo/targetvideotest/exemplary/video.json new file mode 100644 index 00000000000..fcb6e34ed3d --- /dev/null +++ b/adapters/targetVideo/targetvideotest/exemplary/video.json @@ -0,0 +1,122 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "77777" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"] + }, + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "prebid": { + "storedrequest": { + "id": "77777" + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + }, + "cur": ["EUR"] + }, + "impIDs":["test-imp-id-video"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-video", + "seatbid": [ + { + "seat": "targetVideo", + "bid": [ + { + "id": "randomID", + "impid": "test-imp-id-video", + "price": 5.5, + "adm": "test-imp-id-video", + "cid": "789", + "crid": "12345678", + "h": 360, + "w": 640 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids" : [{ + "bid": { + "id": "randomID", + "impid": "test-imp-id-video", + "price": 5.5, + "adm": "test-imp-id-video", + "crid": "12345678", + "cid": "789", + "h": 360, + "w": 640 + }, + "type": "video" + }] + } + ] +} diff --git a/adapters/targetVideo/targetvideotest/supplemental/204.json b/adapters/targetVideo/targetvideotest/supplemental/204.json new file mode 100644 index 00000000000..c64d797b32c --- /dev/null +++ b/adapters/targetVideo/targetvideotest/supplemental/204.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "77777" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"] + }, + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "prebid": { + "storedrequest": { + "id": "77777" + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + }, + "cur": ["EUR"] + }, + "impIDs":["test-imp-id-video"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/targetVideo/targetvideotest/supplemental/400.json b/adapters/targetVideo/targetvideotest/supplemental/400.json new file mode 100644 index 00000000000..e69838a3c1d --- /dev/null +++ b/adapters/targetVideo/targetvideotest/supplemental/400.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "77777" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"] + }, + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "prebid": { + "storedrequest": { + "id": "77777" + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + }, + "cur": ["EUR"] + }, + "impIDs":["test-imp-id-video"] + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/targetVideo/targetvideotest/supplemental/ext-bidder-error.json b/adapters/targetVideo/targetvideotest/supplemental/ext-bidder-error.json new file mode 100644 index 00000000000..81e6a5742ad --- /dev/null +++ b/adapters/targetVideo/targetvideotest/supplemental/ext-bidder-error.json @@ -0,0 +1,39 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Invalid ext.bidder", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "prebid": { + "placementId": "test" + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/targetVideo/targetvideotest/supplemental/invalid-placement-id.json b/adapters/targetVideo/targetvideotest/supplemental/invalid-placement-id.json new file mode 100644 index 00000000000..5e02af68455 --- /dev/null +++ b/adapters/targetVideo/targetvideotest/supplemental/invalid-placement-id.json @@ -0,0 +1,41 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Placement ID missing", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/targetVideo/targetvideotest/supplemental/no_seatbid.json b/adapters/targetVideo/targetvideotest/supplemental/no_seatbid.json new file mode 100644 index 00000000000..73321a58380 --- /dev/null +++ b/adapters/targetVideo/targetvideotest/supplemental/no_seatbid.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "cur": ["EUR"], + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "bidder": { + "placementId": "77777" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": ["application/json"], + "Content-Type": ["application/json;charset=utf-8"] + }, + "uri": "http://localhost/pbs", + "body": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 360 + }, + "ext": { + "prebid": { + "storedrequest": { + "id": "77777" + } + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path", + "ext": { + "amp": 0 + } + }, + "cur": ["EUR"] + }, + "impIDs":["test-imp-id-video"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [{ + "currency": "USD" + }] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 3cb651bcbdb..b3e7cc9f651 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -226,6 +226,7 @@ import ( "github.com/prebid/prebid-server/v3/adapters/stroeerCore" "github.com/prebid/prebid-server/v3/adapters/taboola" "github.com/prebid/prebid-server/v3/adapters/tappx" + "github.com/prebid/prebid-server/v3/adapters/targetVideo" "github.com/prebid/prebid-server/v3/adapters/teads" "github.com/prebid/prebid-server/v3/adapters/telaria" "github.com/prebid/prebid-server/v3/adapters/teqblaze" @@ -496,6 +497,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderStroeerCore: stroeerCore.Builder, openrtb_ext.BidderTaboola: taboola.Builder, openrtb_ext.BidderTappx: tappx.Builder, + openrtb_ext.BidderTargetVideo: targetVideo.Builder, openrtb_ext.BidderTeads: teads.Builder, openrtb_ext.BidderTelaria: telaria.Builder, openrtb_ext.BidderTeqBlaze: teqblaze.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index e94d0fe5513..5bda9330a46 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -244,6 +244,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderStroeerCore, BidderTaboola, BidderTappx, + BidderTargetVideo, BidderTeads, BidderTelaria, BidderTeqBlaze, @@ -618,6 +619,7 @@ const ( BidderStroeerCore BidderName = "stroeerCore" BidderTaboola BidderName = "taboola" BidderTappx BidderName = "tappx" + BidderTargetVideo BidderName = "targetVideo" BidderTeads BidderName = "teads" BidderTelaria BidderName = "telaria" BidderTeqBlaze BidderName = "teqblaze" diff --git a/openrtb_ext/imp_targetvideo.go b/openrtb_ext/imp_targetvideo.go new file mode 100644 index 00000000000..c09296168ce --- /dev/null +++ b/openrtb_ext/imp_targetvideo.go @@ -0,0 +1,8 @@ +package openrtb_ext + +import "github.com/prebid/prebid-server/v3/util/jsonutil" + +// ExtImpTargetVideo defines the contract for bidrequest.imp[i].ext.prebid.bidder.targetVideo +type ExtImpTargetVideo struct { + PlacementId jsonutil.StringInt `json:"placementId,omitempty"` +} diff --git a/static/bidder-info/targetVideo.yaml b/static/bidder-info/targetVideo.yaml new file mode 100644 index 00000000000..aa935f472a7 --- /dev/null +++ b/static/bidder-info/targetVideo.yaml @@ -0,0 +1,17 @@ +endpoint: "https://pbs.prebrid.tv/openrtb2/auction" +geoscope: + - global +maintainer: + email: "predrag.milosevic@target-video.com" +gvlVendorID: 786 +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video +userSync: + redirect: + url: "https://pbs.prebrid.tv/user_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}" + userMacro: "[TVUID]" diff --git a/static/bidder-params/targetVideo.json b/static/bidder-params/targetVideo.json new file mode 100644 index 00000000000..86c73b4bb7b --- /dev/null +++ b/static/bidder-params/targetVideo.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TargetVideo Adapter Params", + "description": "A schema which validates params accepted by the TargetVideo adapter", + "type": "object", + "properties": { + "placementId": { + "type": ["integer", "string"], + "pattern": "^\\d+$", + "description": "An ID which identifies this placement of the impression" + } + }, + "required": ["placementId"] +}