diff --git a/adapters/aniview/aniview.go b/adapters/aniview/aniview.go new file mode 100644 index 00000000000..cc2bfdc7c77 --- /dev/null +++ b/adapters/aniview/aniview.go @@ -0,0 +1,199 @@ +package aniview + +import ( + "bytes" + "fmt" + "net/http" + "strings" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/errortypes" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/util/jsonutil" +) + +type adapter struct { + endpoint string +} + +type aniviewExt struct { + PBS int `json:"pbs"` +} + +// Builder builds a new instance of the Aniview adapter for the given bidder with the given config. +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) { + var requests []*adapters.RequestData + var errors []error + + requestExt, err := buildRequestExt(request.Ext) + if err != nil { + return nil, []error{err} + } + + // One outgoing request per imp and per media type, mirroring the Prebid.js adapter. + for _, imp := range request.Imp { + impExt, err := extractImpExt(&imp) + if err != nil { + errors = append(errors, err) + continue + } + + imp.TagID = strings.TrimSpace(impExt.ChannelId) + + for _, singleTypeImp := range splitImpByMediaType(imp) { + requestCopy := *request + requestCopy.Imp = []openrtb2.Imp{singleTypeImp} + requestCopy.Ext = requestExt + + requestJSON, err := jsonutil.Marshal(&requestCopy) + if err != nil { + errors = append(errors, fmt.Errorf("marshal bidRequest: %w", err)) + continue + } + + // Headers must be a fresh map per request: RequestData.Headers is + // mutable and shared maps would leak mutations across requests. + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requests = append(requests, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + Headers: headers, + ImpIDs: []string{singleTypeImp.ID}, + }) + } + } + + return requests, errors +} + +// buildRequestExt merges ext.aniview into the existing request.ext, preserving +// whatever PBS core has put there. +func buildRequestExt(requestExt []byte) ([]byte, error) { + extMap := map[string]interface{}{} + if len(requestExt) > 0 { + if err := jsonutil.Unmarshal(requestExt, &extMap); err != nil { + return nil, fmt.Errorf("unmarshal request.ext: %w", err) + } + } + + extMap["aniview"] = aniviewExt{PBS: 1} + + ext, err := jsonutil.Marshal(extMap) + if err != nil { + return nil, fmt.Errorf("marshal request.ext: %w", err) + } + return ext, nil +} + +// splitImpByMediaType returns one imp per media type, so each outgoing request +// carries a single media type, as the Aniview endpoint expects. +func splitImpByMediaType(imp openrtb2.Imp) []openrtb2.Imp { + if imp.Video == nil || imp.Banner == nil { + return []openrtb2.Imp{imp} + } + + videoImp := imp + videoImp.Banner = nil + bannerImp := imp + bannerImp.Video = nil + return []openrtb2.Imp{videoImp, bannerImp} +} + +func extractImpExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtAniview, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, fmt.Errorf("unmarshal bidderExt: %w", err) + } + + var impExt openrtb_ext.ImpExtAniview + if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + return nil, fmt.Errorf("unmarshal ImpExtAniview: %w", err) + } + + if strings.TrimSpace(impExt.ChannelId) == "" { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Missing AV_CHANNELID for imp: %s", imp.ID), + } + } + return &impExt, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + // The exchange may answer a no-bid as 200 with an empty/whitespace body. + if len(bytes.TrimSpace(responseData.Body)) == 0 { + return nil, nil + } + + var response openrtb2.BidResponse + if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("bad server response: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(response.SeatBid)) + + if response.Cur != "" { + bidResponse.Currency = response.Cur + } + + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + // Mirror the Prebid.js adapter: a bid without markup or a VAST url is unusable. + if bid.AdM == "" && bid.NURL == "" { + continue + } + + bidType, err := getMediaTypeForBid(&bid) + if err != nil { + errs = append(errs, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} + +// getMediaTypeForBid resolves the bid media type from bid.mtype, which the +// exchange sets explicitly on every bid. +func getMediaTypeForBid(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Could not define bid type for imp: %s", bid.ImpID), + } + } +} diff --git a/adapters/aniview/aniview_test.go b/adapters/aniview/aniview_test.go new file mode 100644 index 00000000000..79780211355 --- /dev/null +++ b/adapters/aniview/aniview_test.go @@ -0,0 +1,48 @@ +package aniview + +import ( + "testing" + + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/adapters/adapterstest" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestBuildRequestExtInvalid(t *testing.T) { + if _, err := buildRequestExt([]byte(`"not-an-object"`)); err == nil { + t.Error("expected error for non-object request.ext") + } + ext, err := buildRequestExt([]byte(`{"prebid":{"debug":true}}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(ext) == "" { + t.Error("expected merged ext") + } +} + +func TestMakeBidsEmptyBody(t *testing.T) { + bidder, _ := Builder(openrtb_ext.BidderAniview, config.Adapter{Endpoint: "https://rtb.aniview.com/sspRTB2"}, config.Server{}) + for _, body := range []string{"", "\n", " \n"} { + resp, errs := bidder.(*adapter).MakeBids(nil, &adapters.RequestData{Body: []byte("{}")}, &adapters.ResponseData{StatusCode: 200, Body: []byte(body)}) + if resp != nil || errs != nil { + t.Errorf("empty body %q should be a silent no-bid, got resp=%v errs=%v", body, resp, errs) + } + } +} + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAniview, config.Adapter{ + Endpoint: "https://rtb.aniview.com/sspRTB2", + }, + config.Server{ + ExternalUrl: "http://hosturl.com", GvlID: 780, DataCenter: "2", + }) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "aniviewtest", bidder) +} diff --git a/adapters/aniview/aniviewtest/exemplary/banner.json b/adapters/aniview/aniviewtest/exemplary/banner.json new file mode 100644 index 00000000000..3ca49d136eb --- /dev/null +++ b/adapters/aniview/aniviewtest/exemplary/banner.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "banner-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "banner-imp-id", + "banner": { + "w": 300, + "h": 250, + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "banner-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "banner-imp-id", + "tagid": "abcdef1234567890abcdef12", + "banner": { + "w": 300, + "h": 250, + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": ["banner-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "banner-request-id", + "seatbid": [ + { + "seat": "1234567890abcdef12345678", + "bid": [ + { + "id": "banner-bid-id", + "impid": "banner-imp-id", + "price": 1.2, + "adm": "
", + "crid": "banner-creative-id", + "adomain": ["advertiser.com"], + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "banner-bid-id", + "impid": "banner-imp-id", + "price": 1.2, + "adm": "
", + "crid": "banner-creative-id", + "adomain": ["advertiser.com"], + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/aniview/aniviewtest/exemplary/multi-format.json b/adapters/aniview/aniviewtest/exemplary/multi-format.json new file mode 100644 index 00000000000..25d83df471e --- /dev/null +++ b/adapters/aniview/aniviewtest/exemplary/multi-format.json @@ -0,0 +1,172 @@ +{ + "mockBidRequest": { + "id": "multi-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "multi-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "multi-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "multi-imp-id", + "tagid": "abcdef1234567890abcdef12", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": ["multi-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "video-bid-id", + "impid": "multi-imp-id", + "price": 3.1, + "adm": "Aniview", + "mtype": 2 + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "multi-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "multi-imp-id", + "tagid": "abcdef1234567890abcdef12", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": ["multi-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "banner-bid-id", + "impid": "multi-imp-id", + "price": 0.9, + "adm": "
banner creative
", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "video-bid-id", + "impid": "multi-imp-id", + "price": 3.1, + "adm": "Aniview", + "mtype": 2 + }, + "type": "video" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "banner-bid-id", + "impid": "multi-imp-id", + "price": 0.9, + "adm": "
banner creative
", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/aniview/aniviewtest/exemplary/video.json b/adapters/aniview/aniviewtest/exemplary/video.json new file mode 100644 index 00000000000..43fcf510fdf --- /dev/null +++ b/adapters/aniview/aniviewtest/exemplary/video.json @@ -0,0 +1,121 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "device": { + "ua": "Mozilla/5.0", + "ip": "1.2.3.4" + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "protocols": [2, 3, 5, 6] + }, + "bidfloor": 0.5, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "tmax": 3000 + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "some-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "device": { + "ua": "Mozilla/5.0", + "ip": "1.2.3.4" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "abcdef1234567890abcdef12", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "protocols": [2, 3, 5, 6] + }, + "bidfloor": 0.5, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + }, + "tmax": 3000 + }, + "impIDs": ["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "1234567890abcdef12345678", + "bid": [ + { + "id": "some-bid-id", + "impid": "some-impression-id", + "price": 2.5, + "adm": "AniviewAd00:00:15", + "adid": "some-adsource-id", + "crid": "some-creative-id", + "adomain": ["advertiser.com"], + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "some-bid-id", + "impid": "some-impression-id", + "price": 2.5, + "adm": "AniviewAd00:00:15", + "adid": "some-adsource-id", + "crid": "some-creative-id", + "adomain": ["advertiser.com"], + "mtype": 2 + }, + "type": "video" + } + ] + } + ], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/aniview/aniviewtest/supplemental/bad-imp-ext.json b/adapters/aniview/aniviewtest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..f12b52667de --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/bad-imp-ext.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": "not-an-object" + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + } + ] + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "^unmarshal ImpExtAniview", + "comparison": "regex" + } + ] +} diff --git a/adapters/aniview/aniviewtest/supplemental/invalid-response.json b/adapters/aniview/aniviewtest/supplemental/invalid-response.json new file mode 100644 index 00000000000..4c1e69101f7 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/invalid-response.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "tagid": "abcdef1234567890abcdef12" + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": [ + "imp-1" + ] + }, + "mockResponse": { + "status": 200, + "body": "not-a-bid-response" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "^bad server response", + "comparison": "regex" + } + ] +} diff --git a/adapters/aniview/aniviewtest/supplemental/missing-channelid.json b/adapters/aniview/aniviewtest/supplemental/missing-channelid.json new file mode 100644 index 00000000000..31b88f5c823 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/missing-channelid.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": " " + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + } + ] + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Missing AV_CHANNELID for imp: imp-1", + "comparison": "literal" + } + ] +} diff --git a/adapters/aniview/aniviewtest/supplemental/missing-mtype.json b/adapters/aniview/aniviewtest/supplemental/missing-mtype.json new file mode 100644 index 00000000000..23eb4509c57 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/missing-mtype.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + } + ], + "ext": { + "prebid": { + "debug": true + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "tagid": "abcdef1234567890abcdef12" + } + ], + "ext": { + "prebid": { + "debug": true + }, + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": [ + "imp-1" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "b-mtype", + "impid": "imp-1", + "price": 2.0, + "adm": "", + "mtype": 2 + }, + { + "id": "b-sniff", + "impid": "other-imp", + "price": 1.5, + "adm": " " + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "b-mtype", + "impid": "imp-1", + "price": 2.0, + "adm": "", + "mtype": 2 + }, + "type": "video" + } + ] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Could not define bid type for imp: other-imp", + "comparison": "literal" + } + ] +} diff --git a/adapters/aniview/aniviewtest/supplemental/no-bid.json b/adapters/aniview/aniviewtest/supplemental/no-bid.json new file mode 100644 index 00000000000..f5ef9d9f15b --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/no-bid.json @@ -0,0 +1,74 @@ +{ + "mockBidRequest": { + "id": "nobid-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "nobid-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "nobid-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "nobid-imp-id", + "tagid": "abcdef1234567890abcdef12", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": ["nobid-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "nobid-request-id", + "seatbid": [] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/aniview/aniviewtest/supplemental/no-content.json b/adapters/aniview/aniviewtest/supplemental/no-content.json new file mode 100644 index 00000000000..ce70c188d23 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/no-content.json @@ -0,0 +1,72 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "tagid": "abcdef1234567890abcdef12" + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": [ + "imp-1" + ] + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/aniview/aniviewtest/supplemental/nurl-video.json b/adapters/aniview/aniviewtest/supplemental/nurl-video.json new file mode 100644 index 00000000000..698a806d367 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/nurl-video.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "nurl-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "nurl-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "nurl-request-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "nurl-imp-id", + "tagid": "abcdef1234567890abcdef12", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + } + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": ["nurl-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "nurl-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "nurl-bid-id", + "impid": "nurl-imp-id", + "price": 1.7, + "nurl": "https://rtb.aniview.com/vast?bid=nurl-bid-id", + "mtype": 2 + }, + { + "id": "empty-bid-id", + "impid": "nurl-imp-id", + "price": 1.1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "nurl-bid-id", + "impid": "nurl-imp-id", + "price": 1.7, + "nurl": "https://rtb.aniview.com/vast?bid=nurl-bid-id", + "mtype": 2 + }, + "type": "video" + } + ] + } + ], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/aniview/aniviewtest/supplemental/status-400.json b/adapters/aniview/aniviewtest/supplemental/status-400.json new file mode 100644 index 00000000000..760977daf9f --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/status-400.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "tagid": "abcdef1234567890abcdef12" + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": [ + "imp-1" + ] + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/aniview/aniviewtest/supplemental/unknown-bid-type.json b/adapters/aniview/aniviewtest/supplemental/unknown-bid-type.json new file mode 100644 index 00000000000..78d5e8d94e8 --- /dev/null +++ b/adapters/aniview/aniviewtest/supplemental/unknown-bid-type.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "banner": { + "w": 300, + "h": 250 + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://rtb.aniview.com/sspRTB2", + "body": { + "id": "req-id", + "site": { + "page": "https://publisher.com/page" + }, + "imp": [ + { + "id": "imp-1", + "ext": { + "bidder": { + "AV_PUBLISHERID": "1234567890abcdef12345678", + "AV_CHANNELID": "abcdef1234567890abcdef12" + } + }, + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "abcdef1234567890abcdef12" + } + ], + "ext": { + "aniview": { + "pbs": 1 + } + } + }, + "impIDs": [ + "imp-1" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "id": "b1", + "impid": "other-imp", + "price": 1.0, + "adm": "plain text creative" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Could not define bid type for imp: other-imp", + "comparison": "literal" + } + ] +} diff --git a/adapters/aniview/params_test.go b/adapters/aniview/params_test.go new file mode 100644 index 00000000000..ebbeabb1839 --- /dev/null +++ b/adapters/aniview/params_test.go @@ -0,0 +1,48 @@ +package aniview + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v4/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 _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderAniview, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected valid 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 schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAniview, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"AV_PUBLISHERID": "1234567890abcdef12345678", "AV_CHANNELID": "abcdef1234567890abcdef12"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `{}`, + `{"AV_PUBLISHERID": "1234567890abcdef12345678"}`, + `{"AV_CHANNELID": "abcdef1234567890abcdef12"}`, + `{"AV_PUBLISHERID": 123, "AV_CHANNELID": "abcdef1234567890abcdef12"}`, + `{"AV_PUBLISHERID": "", "AV_CHANNELID": "abcdef1234567890abcdef12"}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 75a848b7596..8f0f63b5bc6 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -40,6 +40,7 @@ import ( "github.com/prebid/prebid-server/v4/adapters/alkimi" alliance_gravity "github.com/prebid/prebid-server/v4/adapters/alliance_gravity" "github.com/prebid/prebid-server/v4/adapters/amx" + "github.com/prebid/prebid-server/v4/adapters/aniview" "github.com/prebid/prebid-server/v4/adapters/apacdex" "github.com/prebid/prebid-server/v4/adapters/appnexus" "github.com/prebid/prebid-server/v4/adapters/appush" @@ -312,6 +313,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAlkimi: alkimi.Builder, openrtb_ext.BidderAllianceGravity: alliance_gravity.Builder, openrtb_ext.BidderAMX: amx.Builder, + openrtb_ext.BidderAniview: aniview.Builder, openrtb_ext.BidderApacdex: apacdex.Builder, openrtb_ext.BidderAppnexus: appnexus.Builder, openrtb_ext.BidderAppush: appush.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index aa725e7d1bd..874bb71b37f 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -56,6 +56,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAlkimi, BidderAllianceGravity, BidderAMX, + BidderAniview, BidderApacdex, BidderAppnexus, BidderAppush, @@ -435,6 +436,7 @@ const ( BidderAlkimi BidderName = "alkimi" BidderAllianceGravity BidderName = "alliance_gravity" BidderAMX BidderName = "amx" + BidderAniview BidderName = "aniview" BidderApacdex BidderName = "apacdex" BidderAppnexus BidderName = "appnexus" BidderAppush BidderName = "appush" diff --git a/openrtb_ext/imp_aniview.go b/openrtb_ext/imp_aniview.go new file mode 100644 index 00000000000..7b6fd80e51b --- /dev/null +++ b/openrtb_ext/imp_aniview.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ImpExtAniview defines the contract for bidrequest.imp[i].ext.prebid.bidder.aniview +type ImpExtAniview struct { + PublisherId string `json:"AV_PUBLISHERID"` + ChannelId string `json:"AV_CHANNELID"` +} diff --git a/static/bidder-info/aniview.yaml b/static/bidder-info/aniview.yaml new file mode 100644 index 00000000000..5e16d1ac8b3 --- /dev/null +++ b/static/bidder-info/aniview.yaml @@ -0,0 +1,18 @@ +endpoint: "https://rtb.aniview.com/sspRTB2" +maintainer: + email: "support@aniview.com" +gvlVendorID: 780 +endpointCompression: gzip +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + iframe: + url: https://player.aniview.com/ssync/ssync.html?pbs=1&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&st=iframe&r={{.RedirectURL}} + userMacro: "[AV_UID]" diff --git a/static/bidder-params/aniview.json b/static/bidder-params/aniview.json new file mode 100644 index 00000000000..6c0bd48bd66 --- /dev/null +++ b/static/bidder-params/aniview.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Aniview Adapter Params", + "description": "A schema which validates params accepted by the Aniview adapter", + "type": "object", + "properties": { + "AV_PUBLISHERID": { + "type": "string", + "description": "The Aniview publisher (network) id.", + "minLength": 1 + }, + "AV_CHANNELID": { + "type": "string", + "description": "The Aniview channel id.", + "minLength": 1 + } + }, + "required": [ + "AV_PUBLISHERID", + "AV_CHANNELID" + ] +}