diff --git a/adapters/epom_as/epom_as.go b/adapters/epom_as/epom_as.go new file mode 100644 index 00000000000..fb90f0429b0 --- /dev/null +++ b/adapters/epom_as/epom_as.go @@ -0,0 +1,325 @@ +package epom_as + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "text/template" + + "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/macros" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/util/jsonutil" + "github.com/prebid/prebid-server/v4/util/urlutil" +) + +// adapter talks to the Epom Ad Server, the sell side of the Epom platform. +// It is a different product from the `epom` adapter, which is the Epom DSP: +// the DSP buys impressions, this one sells a publisher's own inventory. +// +// Epom is white-label, so every network serves from its own domain and the +// host arrives per impression in imp.ext.bidder.host rather than from config. +type adapter struct { + endpointTemplate *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, cfg config.Adapter, server config.Server) (adapters.Bidder, error) { + endpointTemplate, err := template.New("endpointTemplate").Parse(cfg.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + return &adapter{endpointTemplate: endpointTemplate}, nil +} + +// MakeRequests emits one request per host, carrying every impression addressed +// to that host. +// +// Keeping a host's impressions together is a requirement, not an optimisation: +// the ad server decides a page as a unit, so its roadblock and +// one-campaign-per-page rules only hold when every slot is resolved in the same +// auction. Splitting per impression would make those rules race each other. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + + // Preserve the order hosts were first seen so the emitted requests are + // deterministic — Go map iteration is not. + hostOrder := make([]string, 0, len(request.Imp)) + impsByHost := make(map[string][]openrtb2.Imp, len(request.Imp)) + + for _, imp := range request.Imp { + impExt, err := parseImpExt(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + // The placement travels as imp.tagid so the wire format is identical to + // the one the Prebid.js adapter sends, and the ad server has a single + // place to read it from. + imp.TagID = impExt.PlacementKey + + applyBidFloor(&imp, impExt) + + if err := enrichImpExt(&imp, impExt); err != nil { + errs = append(errs, err) + continue + } + + if _, seen := impsByHost[impExt.Host]; !seen { + hostOrder = append(hostOrder, impExt.Host) + } + impsByHost[impExt.Host] = append(impsByHost[impExt.Host], imp) + } + + if len(impsByHost) == 0 { + return nil, errs + } + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Accept": {"application/json"}, + } + // The ad server resolves geo, IP targeting and its consent country fallback from the address it + // sees, and a server-to-server call's socket peer is this host, never the reader. Forwarding the + // address the request already carries is what makes those decisions about the right person. + if request.Device != nil { + addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IPv6) + addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IP) + } + + requests := make([]*adapters.RequestData, 0, len(impsByHost)) + for _, host := range hostOrder { + imps := impsByHost[host] + + url, err := macros.ResolveMacros(a.endpointTemplate, macros.EndpointTemplateParams{Host: host}) + if err != nil { + errs = append(errs, err) + continue + } + + hostRequest := *request + hostRequest.Imp = imps + + body, err := jsonutil.Marshal(hostRequest) + if err != nil { + errs = append(errs, err) + continue + } + + requests = append(requests, &adapters.RequestData{ + Method: http.MethodPost, + Uri: url, + Body: body, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(imps), + }) + } + + return requests, errs +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{err} + } + + var bidResponse openrtb2.BidResponse + if err := jsonutil.Unmarshal(response.Body, &bidResponse); err != nil { + return nil, []error{&errortypes.BadServerResponse{Message: err.Error()}} + } + + impsByID := make(map[string]*openrtb2.Imp, len(request.Imp)) + for i := range request.Imp { + impsByID[request.Imp[i].ID] = &request.Imp[i] + } + + result := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if bidResponse.Cur != "" { + result.Currency = bidResponse.Cur + } + + var errs []error + for _, seatBid := range bidResponse.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForBid(seatBid.Bid[i], impsByID) + if err != nil { + errs = append(errs, err) + continue + } + result.Bids = append(result.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + return result, errs +} + +func addHeaderIfNonEmpty(headers http.Header, name, value string) { + if value != "" { + headers.Add(name, value) + } +} + +// applyBidFloor fills the floor from the bidder params only when the request +// carries none of its own, so a floor already resolved by the Price Floors +// module — or set by the publisher on the impression — always wins. +func applyBidFloor(imp *openrtb2.Imp, impExt *openrtb_ext.ExtImpEpomAs) { + if imp.BidFloor != 0 || impExt.BidFloor <= 0 { + return + } + imp.BidFloor = impExt.BidFloor + if impExt.BidFloorCur != "" { + imp.BidFloorCur = impExt.BidFloorCur + } else { + imp.BidFloorCur = "USD" + } +} + +// enrichImpExt moves the Epom-specific params out of imp.ext.bidder and into the +// shape the ad server reads: channel under our own namespace, custom parameters +// merged into imp.ext.data, the standard first-party-data home, so that data +// contributed by RTD modules lands in the same object. +func enrichImpExt(imp *openrtb2.Imp, impExt *openrtb_ext.ExtImpEpomAs) error { + if impExt.Channel == "" && len(impExt.CustomParams) == 0 { + return nil + } + + ext := map[string]json.RawMessage{} + if len(imp.Ext) > 0 { + if err := jsonutil.Unmarshal(imp.Ext, &ext); err != nil { + return &errortypes.BadInput{Message: fmt.Sprintf("imp %s: malformed ext: %s", imp.ID, err.Error())} + } + } + + if impExt.Channel != "" { + namespace, err := jsonutil.Marshal(map[string]string{"channel": impExt.Channel}) + if err != nil { + return err + } + ext[string(openrtb_ext.BidderEpomAs)] = namespace + } + + if merged := mergeCustomParams(ext["data"], impExt.CustomParams); merged != nil { + ext["data"] = merged + } + + encoded, err := jsonutil.Marshal(ext) + if err != nil { + return err + } + imp.Ext = encoded + return nil +} + +// mergeCustomParams folds the custom params into any existing imp.ext.data. +// Existing keys win — data already on the imp came from the publisher's own +// first-party configuration. Values are stringified because the ad server reads +// custom targeting as text; the schema already restricts them to scalars, so a +// value this cannot stringify only reaches here through a host that skipped +// param validation, and is skipped rather than written as a Go rendering of a +// map. Nothing is dropped for size, which is what keeps the marshalled imp.ext +// independent of Go's randomised map iteration order. +func mergeCustomParams(existing json.RawMessage, params map[string]interface{}) json.RawMessage { + out := map[string]interface{}{} + for key, value := range params { + if asString, ok := scalarToString(value); ok { + out[key] = asString + } + } + if len(out) == 0 { + return nil + } + + if len(existing) > 0 { + current := map[string]interface{}{} + if err := jsonutil.Unmarshal(existing, ¤t); err == nil { + for key, value := range current { + out[key] = value + } + } + } + + encoded, err := jsonutil.Marshal(out) + if err != nil { + return nil + } + return encoded +} + +func scalarToString(value interface{}) (string, bool) { + switch v := value.(type) { + case string: + return v, true + case bool: + return strconv.FormatBool(v), true + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), true + case json.Number: + return v.String(), true + default: + return "", false + } +} + +func parseImpExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpEpomAs, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: missing bidder ext: %s", imp.ID, err.Error()), + } + } + + var impExt openrtb_ext.ExtImpEpomAs + if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: cannot resolve host or placementKey: %s", imp.ID, err.Error()), + } + } + + // The host is the only publisher-controlled part of the outbound URL, so it + // must be a bare hostname; anything carrying a path, query or userinfo could + // redirect the bid request to an unintended destination. + if !urlutil.IsSafeHost(impExt.Host) { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: invalid host", imp.ID), + } + } + if impExt.PlacementKey == "" { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: missing placementKey", imp.ID), + } + } + + return &impExt, nil +} + +// getMediaTypeForBid resolves the bid's media type from mtype, falling back to +// the impression the bid answers when the ad server omits it. Nothing is +// assumed: a bid that matches no banner impression is a defect on the wire, and +// rendering it as a banner would hide that. +func getMediaTypeForBid(bid openrtb2.Bid, impsByID map[string]*openrtb2.Imp) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case 0: + if imp, ok := impsByID[bid.ImpID]; ok && imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("unresolved mtype for bid %s: no banner imp %s", bid.ID, bid.ImpID), + } + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("unsupported mtype %d for bid %s", bid.MType, bid.ID), + } + } +} diff --git a/adapters/epom_as/epom_as_test.go b/adapters/epom_as/epom_as_test.go new file mode 100644 index 00000000000..a85c46be5c4 --- /dev/null +++ b/adapters/epom_as/epom_as_test.go @@ -0,0 +1,273 @@ +package epom_as + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "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/errortypes" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testEndpoint = "https://{{.Host}}/hb/bid" + +// End-to-end MakeRequests/MakeBids behaviour is exercised by the JSON fixtures +// under epom_astest/exemplary and epom_astest/supplemental. The Go tests below +// cover only what fixtures cannot: the error-TYPE contract (BadInput vs +// BadServerResponse, which fixtures compare by message only), the promise that +// the caller's request is never mutated, and the determinism of the custom-param +// merge, which a single fixture run cannot distinguish from luck. + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderEpomAs, + config.Adapter{Endpoint: testEndpoint}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 849, DataCenter: "2"}, + ) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "epom_astest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder( + openrtb_ext.BidderEpomAs, + config.Adapter{Endpoint: "{{Malformed}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 849, DataCenter: "2"}, + ) + + assert.Error(t, buildErr) +} + +func TestMakeRequestsErrorsAreBadInput(t *testing.T) { + testCases := []struct { + name string + impExt json.RawMessage + message string + }{ + { + name: "imp.ext is not an object", + impExt: json.RawMessage(`"not-an-object"`), + message: "imp test-imp-id: missing bidder ext", + }, + { + name: "imp.ext.bidder is not an object", + impExt: json.RawMessage(`{"bidder":"not-an-object"}`), + message: "imp test-imp-id: cannot resolve host or placementKey", + }, + { + name: "host would rewrite the outbound url", + impExt: json.RawMessage(`{"bidder":{"host":"ads.example.com/collect","placementKey":"a4f21c9e7b"}}`), + message: "imp test-imp-id: invalid host", + }, + { + name: "host is absent", + impExt: json.RawMessage(`{"bidder":{"placementKey":"a4f21c9e7b"}}`), + message: "imp test-imp-id: invalid host", + }, + { + name: "placementKey is absent", + impExt: json.RawMessage(`{"bidder":{"host":"ads.example.com"}}`), + message: "imp test-imp-id: missing placementKey", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + requests, errs := newAdapter(t).MakeRequests(givenRequest(givenImp("test-imp-id", tc.impExt)), &adapters.ExtraRequestInfo{}) + + assert.Empty(t, requests) + require.Len(t, errs, 1) + assert.IsType(t, &errortypes.BadInput{}, errs[0]) + assert.Contains(t, errs[0].Error(), tc.message) + }) + } +} + +func TestMakeBidsErrorTypes(t *testing.T) { + request := givenRequest(givenImp("test-imp-id", json.RawMessage(`{"bidder":{"host":"ads.example.com","placementKey":"a4f21c9e7b"}}`))) + + testCases := []struct { + name string + status int + body string + wantType error + }{ + { + name: "4xx is the publisher's problem", + status: http.StatusBadRequest, + body: `{}`, + wantType: &errortypes.BadInput{}, + }, + { + name: "5xx is the exchange's problem", + status: http.StatusInternalServerError, + body: `{}`, + wantType: &errortypes.BadServerResponse{}, + }, + { + name: "unparseable body", + status: http.StatusOK, + body: `not json at all`, + wantType: &errortypes.BadServerResponse{}, + }, + { + name: "unsupported mtype", + status: http.StatusOK, + body: `{"id":"test-request-id","cur":"USD","seatbid":[{"bid":[{"id":"b1","impid":"test-imp-id","price":1,"mtype":2}]}]}`, + wantType: &errortypes.BadServerResponse{}, + }, + { + name: "no mtype and no matching imp", + status: http.StatusOK, + body: `{"id":"test-request-id","cur":"USD","seatbid":[{"bid":[{"id":"b1","impid":"no-such-imp","price":1}]}]}`, + wantType: &errortypes.BadServerResponse{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + response := &adapters.ResponseData{StatusCode: tc.status, Body: []byte(tc.body)} + + _, errs := newAdapter(t).MakeBids(request, &adapters.RequestData{}, response) + + require.Len(t, errs, 1) + assert.IsType(t, tc.wantType, errs[0]) + }) + } +} + +// TestCallerRequestNotMutated pins the copy-on-write surface: MakeRequests +// rewrites tagid, the floor and imp.ext, and the exchange reuses the same +// request object for every bidder in the auction. +func TestCallerRequestNotMutated(t *testing.T) { + impExt := json.RawMessage(`{"bidder":{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":"sports-uk","customParams":{"section":"sport"},"bidFloor":2.5,"bidFloorCur":"EUR"}}`) + request := givenRequest(givenImp("test-imp-id", impExt)) + + requests, errs := newAdapter(t).MakeRequests(request, &adapters.ExtraRequestInfo{}) + assert.Empty(t, errs) + require.Len(t, requests, 1) + + assert.Empty(t, request.Imp[0].TagID, "caller's imp[0].TagID must not be mutated") + assert.EqualValues(t, 0, request.Imp[0].BidFloor, "caller's imp[0].BidFloor must not be mutated") + assert.Empty(t, request.Imp[0].BidFloorCur, "caller's imp[0].BidFloorCur must not be mutated") + assert.JSONEq(t, string(impExt), string(request.Imp[0].Ext), "caller's imp[0].Ext must not be mutated") + assert.Len(t, request.Imp, 1, "caller's imp slice must not be re-sliced") +} + +func TestGetMediaTypeForBid(t *testing.T) { + bannerImp := &openrtb2.Imp{ID: "banner-imp", Banner: &openrtb2.Banner{}} + videoImp := &openrtb2.Imp{ID: "video-imp", Video: &openrtb2.Video{}} + imps := map[string]*openrtb2.Imp{bannerImp.ID: bannerImp, videoImp.ID: videoImp} + + testCases := []struct { + name string + bid openrtb2.Bid + wantType openrtb_ext.BidType + wantErr string + }{ + { + name: "mtype banner", + bid: openrtb2.Bid{ID: "b1", ImpID: "banner-imp", MType: openrtb2.MarkupBanner}, + wantType: openrtb_ext.BidTypeBanner, + }, + { + name: "no mtype resolves from a banner imp", + bid: openrtb2.Bid{ID: "b2", ImpID: "banner-imp"}, + wantType: openrtb_ext.BidTypeBanner, + }, + { + name: "no mtype and the imp is not a banner", + bid: openrtb2.Bid{ID: "b3", ImpID: "video-imp"}, + wantErr: "unresolved mtype for bid b3: no banner imp video-imp", + }, + { + name: "no mtype and no matching imp", + bid: openrtb2.Bid{ID: "b4", ImpID: "no-such-imp"}, + wantErr: "unresolved mtype for bid b4: no banner imp no-such-imp", + }, + { + name: "video mtype on a banner-only adapter", + bid: openrtb2.Bid{ID: "b5", ImpID: "banner-imp", MType: openrtb2.MarkupVideo}, + wantErr: "unsupported mtype 2 for bid b5", + }, + { + name: "native mtype on a banner-only adapter", + bid: openrtb2.Bid{ID: "b6", ImpID: "banner-imp", MType: openrtb2.MarkupNative}, + wantErr: "unsupported mtype 4 for bid b6", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bidType, err := getMediaTypeForBid(tc.bid, imps) + + if tc.wantErr != "" { + require.Error(t, err) + assert.IsType(t, &errortypes.BadServerResponse{}, err) + assert.Equal(t, tc.wantErr, err.Error()) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.wantType, bidType) + }) + } +} + +// TestCustomParamsMergeIsDeterministic guards the reason the adapter caps +// nothing: with no entry dropped, Go's randomised map iteration order cannot +// change which keys reach the wire, so the marshalled body is stable across +// runs. A single fixture pass would not tell a stable result from a lucky one. +func TestCustomParamsMergeIsDeterministic(t *testing.T) { + params := map[string]interface{}{} + for i := 0; i < 40; i++ { + params[fmt.Sprintf("key%02d", i)] = fmt.Sprintf("value-%02d", i) + } + + first := mergeCustomParams(nil, params) + require.NotNil(t, first) + + for i := 0; i < 50; i++ { + assert.Equal(t, string(first), string(mergeCustomParams(nil, params))) + } + + var decoded map[string]string + require.NoError(t, json.Unmarshal(first, &decoded)) + assert.Len(t, decoded, 40, "no custom param may be dropped") +} + +func newAdapter(t *testing.T) adapters.Bidder { + t.Helper() + bidder, err := Builder( + openrtb_ext.BidderEpomAs, + config.Adapter{Endpoint: testEndpoint}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 849, DataCenter: "2"}, + ) + require.NoError(t, err) + return bidder +} + +func givenImp(id string, ext json.RawMessage) openrtb2.Imp { + return openrtb2.Imp{ + ID: id, + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, + Ext: ext, + } +} + +func givenRequest(imps ...openrtb2.Imp) *openrtb2.BidRequest { + return &openrtb2.BidRequest{ + ID: "test-request-id", + Imp: imps, + Site: &openrtb2.Site{Page: "https://publisher.example.com/article"}, + } +} diff --git a/adapters/epom_as/epom_astest/exemplary/bid-floor-params.json b/adapters/epom_as/epom_astest/exemplary/bid-floor-params.json new file mode 100644 index 00000000000..4a66cbdda88 --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/bid-floor-params.json @@ -0,0 +1,143 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-floor-with-currency", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "bidFloor": 1.75, + "bidFloorCur": "EUR" + } + } + }, + { + "id": "imp-floor-defaults-to-usd", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "6d0e83b415", + "bidFloor": 0.5 + } + } + }, + { + "id": "imp-zero-floor-is-no-floor", + "banner": { "format": [{ "w": 970, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "ff0011aa22", + "bidFloor": 0, + "bidFloorCur": "EUR" + } + } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-floor-with-currency", + "tagid": "a4f21c9e7b", + "bidfloor": 1.75, + "bidfloorcur": "EUR", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "bidFloor": 1.75, + "bidFloorCur": "EUR" + } + } + }, + { + "id": "imp-floor-defaults-to-usd", + "tagid": "6d0e83b415", + "bidfloor": 0.5, + "bidfloorcur": "USD", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "6d0e83b415", + "bidFloor": 0.5 + } + } + }, + { + "id": "imp-zero-floor-is-no-floor", + "tagid": "ff0011aa22", + "banner": { "format": [{ "w": 970, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "ff0011aa22", + "bidFloor": 0, + "bidFloorCur": "EUR" + } + } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "impIDs": ["imp-floor-with-currency", "imp-floor-defaults-to-usd", "imp-zero-floor-is-no-floor"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "EUR", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "imp-floor-with-currency", + "price": 2.1, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "imp-floor-with-currency", + "price": 2.1, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/channel-and-custom-params.json b/adapters/epom_as/epom_astest/exemplary/channel-and-custom-params.json new file mode 100644 index 00000000000..cbf8e58a73a --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/channel-and-custom-params.json @@ -0,0 +1,92 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "sports-uk", + "customParams": { "section": "sport", "tier": 2, "premium": true } + } + } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "sports-uk", + "customParams": { "section": "sport", "tier": 2, "premium": true } + }, + "epom_as": { "channel": "sports-uk" }, + "data": { "section": "sport", "tier": "2", "premium": "true" } + } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 2.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 2.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/device-address-forwarded.json b/adapters/epom_as/epom_astest/exemplary/device-address-forwarded.json new file mode 100644 index 00000000000..80ae6c3f8d4 --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/device-address-forwarded.json @@ -0,0 +1,143 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + }, + "tmax": 500, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", + "ip": "203.0.113.44", + "ipv6": "2001:db8::8a2e:370:7334", + "language": "en" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json" + ], + "Accept": [ + "application/json" + ], + "X-Forwarded-For": [ + "2001:db8::8a2e:370:7334", + "203.0.113.44" + ] + }, + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + }, + "tmax": 500, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", + "ip": "203.0.113.44", + "ipv6": "2001:db8::8a2e:370:7334", + "language": "en" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 2.75, + "adm": "
creative
", + "crid": "creative-99", + "dealid": "epom-direct", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 2.75, + "adm": "
creative
", + "crid": "creative-99", + "dealid": "epom-direct", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/multi-host.json b/adapters/epom_as/epom_astest/exemplary/multi-host.json new file mode 100644 index 00000000000..e9c1d585118 --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/multi-host.json @@ -0,0 +1,142 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-network-one", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { "bidder": { "host": "ads.network-one.com", "placementKey": "a4f21c9e7b" } } + }, + { + "id": "imp-network-two", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.network-two.com", "placementKey": "6d0e83b415" } } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.network-one.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-network-one", + "tagid": "a4f21c9e7b", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { "bidder": { "host": "ads.network-one.com", "placementKey": "a4f21c9e7b" } } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "impIDs": ["imp-network-one"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "imp-network-one", + "price": 1.5, + "adm": "
a
", + "crid": "c1", + "w": 728, + "h": 90, + "mtype": 1 + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://ads.network-two.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-network-two", + "tagid": "6d0e83b415", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.network-two.com", "placementKey": "6d0e83b415" } } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "impIDs": ["imp-network-two"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-2", + "impid": "imp-network-two", + "price": 2.25, + "adm": "
b
", + "crid": "c2", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "imp-network-one", + "price": 1.5, + "adm": "
a
", + "crid": "c1", + "w": 728, + "h": 90, + "mtype": 1 + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-2", + "impid": "imp-network-two", + "price": 2.25, + "adm": "
b
", + "crid": "c2", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/multi-imp-one-request.json b/adapters/epom_as/epom_astest/exemplary/multi-imp-one-request.json new file mode 100644 index 00000000000..b1f0611090b --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/multi-imp-one-request.json @@ -0,0 +1,126 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-leaderboard", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + }, + { + "id": "imp-sidebar", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "6d0e83b415" } } + }, + { + "id": "imp-footer", + "banner": { "format": [{ "w": 970, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "ff0011aa22" } } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-leaderboard", + "tagid": "a4f21c9e7b", + "banner": { "format": [{ "w": 728, "h": 90 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + }, + { + "id": "imp-sidebar", + "tagid": "6d0e83b415", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "6d0e83b415" } } + }, + { + "id": "imp-footer", + "tagid": "ff0011aa22", + "banner": { "format": [{ "w": 970, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "ff0011aa22" } } + } + ], + "site": { "page": "https://publisher.example.com/article" } + }, + "impIDs": ["imp-leaderboard", "imp-sidebar", "imp-footer"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "imp-leaderboard", + "price": 1.5, + "adm": "
a
", + "crid": "c1", + "dealid": "epom-direct", + "w": 728, + "h": 90, + "mtype": 1 + }, + { + "id": "bid-2", + "impid": "imp-footer", + "price": 4.0, + "adm": "
b
", + "crid": "c2", + "dealid": "epom-direct", + "w": 970, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "imp-leaderboard", + "price": 1.5, + "adm": "
a
", + "crid": "c1", + "dealid": "epom-direct", + "w": 728, + "h": 90, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "bid-2", + "impid": "imp-footer", + "price": 4.0, + "adm": "
b
", + "crid": "c2", + "dealid": "epom-direct", + "w": 970, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/simple-banner-app.json b/adapters/epom_as/epom_astest/exemplary/simple-banner-app.json new file mode 100644 index 00000000000..dcfb71f68dd --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/simple-banner-app.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { "format": [{ "w": 320, "h": 50 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + } + ], + "app": { + "bundle": "com.publisher.reader", + "name": "Publisher Reader", + "publisher": { "id": "pub-1" } + }, + "device": { "ua": "test-ua", "ifa": "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0" } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { "format": [{ "w": 320, "h": 50 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + } + ], + "app": { + "bundle": "com.publisher.reader", + "name": "Publisher Reader", + "publisher": { "id": "pub-1" } + }, + "device": { "ua": "test-ua", "ifa": "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0" } + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 0.9, + "adm": "
creative
", + "crid": "c1", + "adomain": ["advertiser.example"], + "w": 320, + "h": 50, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 0.9, + "adm": "
creative
", + "crid": "c1", + "adomain": ["advertiser.example"], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/exemplary/simple-banner.json b/adapters/epom_as/epom_astest/exemplary/simple-banner.json new file mode 100644 index 00000000000..060684dbed3 --- /dev/null +++ b/adapters/epom_as/epom_astest/exemplary/simple-banner.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { "w": 300, "h": 250 }, + { "w": 300, "h": 600 } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + }, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { "w": 300, "h": 250 }, + { "w": 300, "h": 600 } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + }, + "tmax": 500 + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 2.75, + "adm": "
creative
", + "crid": "creative-99", + "dealid": "epom-direct", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 2.75, + "adm": "
creative
", + "crid": "creative-99", + "dealid": "epom-direct", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/custom-params-do-not-clobber-fpd.json b/adapters/epom_as/epom_astest/supplemental/custom-params-do-not-clobber-fpd.json new file mode 100644 index 00000000000..ea42a24788a --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/custom-params-do-not-clobber-fpd.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "sports-uk", + "customParams": { + "section": "from-param", + "tier": 2 + } + }, + "data": { + "section": "from-first-party", + "keywords": "news" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "sports-uk", + "customParams": { + "section": "from-param", + "tier": 2 + } + }, + "data": { + "section": "from-first-party", + "keywords": "news", + "tier": "2" + }, + "epom_as": { + "channel": "sports-uk" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/custom-params-forty-keys.json b/adapters/epom_as/epom_astest/supplemental/custom-params-forty-keys.json new file mode 100644 index 00000000000..0481681b4bf --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/custom-params-forty-keys.json @@ -0,0 +1,233 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "customParams": { + "key01": 1, + "key02": true, + "key03": "value-03", + "key04": 4, + "key05": false, + "key06": "value-06", + "key07": 7, + "key08": true, + "key09": "value-09", + "key10": 10, + "key11": false, + "key12": "value-12", + "key13": 13, + "key14": true, + "key15": "value-15", + "key16": 16, + "key17": false, + "key18": "value-18", + "key19": 19, + "key20": true, + "key21": "value-21", + "key22": 22, + "key23": false, + "key24": "value-24", + "key25": 25, + "key26": true, + "key27": "value-27", + "key28": 28, + "key29": false, + "key30": "value-30", + "key31": 31, + "key32": true, + "key33": "value-33", + "key34": 34, + "key35": false, + "key36": "value-36", + "key37": 37, + "key38": true, + "key39": "value-39", + "key40": 40 + } + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "customParams": { + "key01": 1, + "key02": true, + "key03": "value-03", + "key04": 4, + "key05": false, + "key06": "value-06", + "key07": 7, + "key08": true, + "key09": "value-09", + "key10": 10, + "key11": false, + "key12": "value-12", + "key13": 13, + "key14": true, + "key15": "value-15", + "key16": 16, + "key17": false, + "key18": "value-18", + "key19": 19, + "key20": true, + "key21": "value-21", + "key22": 22, + "key23": false, + "key24": "value-24", + "key25": 25, + "key26": true, + "key27": "value-27", + "key28": 28, + "key29": false, + "key30": "value-30", + "key31": 31, + "key32": true, + "key33": "value-33", + "key34": 34, + "key35": false, + "key36": "value-36", + "key37": 37, + "key38": true, + "key39": "value-39", + "key40": 40 + } + }, + "data": { + "key01": "1", + "key02": "true", + "key03": "value-03", + "key04": "4", + "key05": "false", + "key06": "value-06", + "key07": "7", + "key08": "true", + "key09": "value-09", + "key10": "10", + "key11": "false", + "key12": "value-12", + "key13": "13", + "key14": "true", + "key15": "value-15", + "key16": "16", + "key17": "false", + "key18": "value-18", + "key19": "19", + "key20": "true", + "key21": "value-21", + "key22": "22", + "key23": "false", + "key24": "value-24", + "key25": "25", + "key26": "true", + "key27": "value-27", + "key28": "28", + "key29": "false", + "key30": "value-30", + "key31": "31", + "key32": "true", + "key33": "value-33", + "key34": "34", + "key35": "false", + "key36": "value-36", + "key37": "37", + "key38": "true", + "key39": "value-39", + "key40": "40" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/empty-channel-ignored.json b/adapters/epom_as/epom_astest/supplemental/empty-channel-ignored.json new file mode 100644 index 00000000000..7582b2de2b2 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/empty-channel-ignored.json @@ -0,0 +1,109 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "channel": "" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/empty-seatbid.json b/adapters/epom_as/epom_astest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..b9201e76df3 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/empty-seatbid.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/invalid-host.json b/adapters/epom_as/epom_astest/supplemental/invalid-host.json new file mode 100644 index 00000000000..317dcee8409 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/invalid-host.json @@ -0,0 +1,15 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com/collect", "placementKey": "a4f21c9e7b" } } + } + ] + }, + "expectedMakeRequestsErrors": [ + { "value": "imp test-imp-id: invalid host", "comparison": "literal" } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext-bidder.json b/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext-bidder.json new file mode 100644 index 00000000000..c9e73372c5c --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext-bidder.json @@ -0,0 +1,31 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": "not-an-object" + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "imp test-imp-id: cannot resolve host or placementKey:.*", + "comparison": "regex" + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext.json b/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext.json new file mode 100644 index 00000000000..e8a2a99c202 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/invalid-imp-ext.json @@ -0,0 +1,29 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": "not-an-object" + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "imp test-imp-id: missing bidder ext:.*", + "comparison": "regex" + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/missing-placement-key.json b/adapters/epom_as/epom_astest/supplemental/missing-placement-key.json new file mode 100644 index 00000000000..ff96e066366 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/missing-placement-key.json @@ -0,0 +1,15 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com" } } + } + ] + }, + "expectedMakeRequestsErrors": [ + { "value": "imp test-imp-id: missing placementKey", "comparison": "literal" } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/multi-imp-partial-failure.json b/adapters/epom_as/epom_astest/supplemental/multi-imp-partial-failure.json new file mode 100644 index 00000000000..a35a3d80df3 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/multi-imp-partial-failure.json @@ -0,0 +1,166 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-good-first", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + }, + { + "id": "imp-bad-host", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com/collect", + "placementKey": "a4f21c9e7b" + } + } + }, + { + "id": "imp-good-second", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "6d0e83b415" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-good-first", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + }, + { + "id": "imp-good-second", + "tagid": "6d0e83b415", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "6d0e83b415" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "imp-good-first", + "imp-good-second" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "imp-good-first", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedMakeRequestsErrors": [ + { + "value": "imp imp-bad-host: invalid host", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "imp-good-first", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/no-content-response.json b/adapters/epom_as/epom_astest/supplemental/no-content-response.json new file mode 100644 index 00000000000..0478544c675 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/no-content-response.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { "format": [{ "w": 300, "h": 250 }] }, + "ext": { "bidder": { "host": "ads.example.com", "placementKey": "a4f21c9e7b" } } + } + ] + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { "status": 204, "body": {} } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/epom_as/epom_astest/supplemental/no-mtype-resolved-from-imp.json b/adapters/epom_as/epom_astest/supplemental/no-mtype-resolved-from-imp.json new file mode 100644 index 00000000000..0ba5977d76b --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/no-mtype-resolved-from-imp.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-no-mtype", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-no-mtype", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/request-floor-wins-over-param.json b/adapters/epom_as/epom_astest/supplemental/request-floor-wins-over-param.json new file mode 100644 index 00000000000..d3f4c58810b --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/request-floor-wins-over-param.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "bidfloor": 1.25, + "bidfloorcur": "GBP", + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "bidFloor": 3.5, + "bidFloorCur": "EUR" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "bidfloor": 1.25, + "bidfloorcur": "GBP", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b", + "bidFloor": 3.5, + "bidFloorCur": "EUR" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/status-400.json b/adapters/epom_as/epom_astest/supplemental/status-400.json new file mode 100644 index 00000000000..974aa9af2ea --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/status-400.json @@ -0,0 +1,74 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "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/epom_as/epom_astest/supplemental/status-500.json b/adapters/epom_as/epom_astest/supplemental/status-500.json new file mode 100644 index 00000000000..6eb89fec919 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/status-500.json @@ -0,0 +1,74 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/unmatched-bid-impid.json b/adapters/epom_as/epom_astest/supplemental/unmatched-bid-impid.json new file mode 100644 index 00000000000..9a0558b5d7d --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/unmatched-bid-impid.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-orphan", + "impid": "no-such-imp", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unresolved mtype for bid bid-orphan: no banner imp no-such-imp", + "comparison": "literal" + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/unparseable-response-body.json b/adapters/epom_as/epom_astest/supplemental/unparseable-response-body.json new file mode 100644 index 00000000000..2a8227570de --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/unparseable-response-body.json @@ -0,0 +1,74 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": "not json at all" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "expect", + "comparison": "regex" + } + ] +} diff --git a/adapters/epom_as/epom_astest/supplemental/unsupported-bid-media-type.json b/adapters/epom_as/epom_astest/supplemental/unsupported-bid-media-type.json new file mode 100644 index 00000000000..e559c4d1bb8 --- /dev/null +++ b/adapters/epom_as/epom_astest/supplemental/unsupported-bid-media-type.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.example.com/hb/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "a4f21c9e7b", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "host": "ads.example.com", + "placementKey": "a4f21c9e7b" + } + } + } + ], + "site": { + "page": "https://publisher.example.com/article" + } + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "epom", + "bid": [ + { + "id": "bid-banner", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + { + "id": "bid-video", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-banner", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
creative
", + "crid": "c1", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unsupported mtype 2 for bid bid-video", + "comparison": "literal" + } + ] +} diff --git a/adapters/epom_as/params_test.go b/adapters/epom_as/params_test.go new file mode 100644 index 00000000000..1c9a3b2341b --- /dev/null +++ b/adapters/epom_as/params_test.go @@ -0,0 +1,127 @@ +package epom_as + +import ( + "encoding/json" + "strings" + "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 _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderEpomAs, 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.BidderEpomAs, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + // host — the pattern is byte-identical to util/urlutil.IsSafeHost, which the + // adapter gates on, so everything it accepts must validate here too. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com:8080","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com:65535","placementKey":"a4f21c9e7b"}`, + `{"host":"ads-eu.example.co.uk","placementKey":"a4f21c9e7b"}`, + // A single-label host is a real deployment shape (an internal name, or + // localhost in a staging rig), not a malformed one. + `{"host":"localhost","placementKey":"a4f21c9e7b"}`, + `{"host":"api-us","placementKey":"a4f21c9e7b"}`, + + // placementKey — minLength 1, so a single character is the boundary. + `{"host":"ads.example.com","placementKey":"a"}`, + + // channel — free-form, and deliberately uncapped: the ad server applies its + // own ingest limits rather than the adapter rejecting the impression. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":"sports-uk"}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":""}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":"` + strings.Repeat("c", 300) + `"}`, + + // customParams — an object of scalars, in every scalar flavour. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{"section":"sport","tier":2,"premium":true}}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{"ratio":1.75,"empty":""}}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{}}`, + + // bidFloor — minimum 0, so 0 is the boundary and means "no floor". + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":0}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":0.01}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":1.75,"bidFloorCur":"EUR"}`, + + // bidFloorCur — a plain string; the schema declares no pattern, so it must + // not reject a currency it merely does not recognise. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloorCur":"USD"}`, + + // Everything at once. + `{"host":"ads.example.com:8443","placementKey":"a4f21c9e7b","channel":"sports-uk","customParams":{"section":"sport"},"bidFloor":2.5,"bidFloorCur":"GBP"}`, +} + +var invalidParams = []string{ + // Non-object roots. + ``, + `null`, + `true`, + `5`, + `[]`, + `"{}"`, + + // Required params. + `{}`, + `{"host":"ads.example.com"}`, + `{"placementKey":"a4f21c9e7b"}`, + + // host — wrong type, and the empty string, which the pattern rejects + // because it demands at least one label character. + `{"host":42,"placementKey":"a4f21c9e7b"}`, + `{"host":"","placementKey":"a4f21c9e7b"}`, + // A host must not be able to rewrite the outbound URL. + `{"host":"https://ads.example.com","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com/collect","placementKey":"a4f21c9e7b"}`, + `{"host":"user@ads.example.com","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com?x=1","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com#frag","placementKey":"a4f21c9e7b"}`, + `{"host":"ads.example.com:80a","placementKey":"a4f21c9e7b"}`, + `{"host":"ads example.com","placementKey":"a4f21c9e7b"}`, + + // placementKey — wrong type, and the empty string just under minLength 1. + `{"host":"ads.example.com","placementKey":42}`, + `{"host":"ads.example.com","placementKey":""}`, + + // channel — wrong type. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":42}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","channel":["sports-uk"]}`, + + // customParams — must be an object of scalars. A nested object or array + // would be stringified into targeting as a Go rendering of a map, so the + // schema rejects the impression instead. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":"not-an-object"}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":[]}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{"nested":{"a":1}}}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{"list":[1,2]}}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","customParams":{"nothing":null}}`, + + // bidFloor — wrong type, and one step under the minimum of 0. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":-1}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":-0.01}`, + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloor":"1.75"}`, + + // bidFloorCur — wrong type. + `{"host":"ads.example.com","placementKey":"a4f21c9e7b","bidFloorCur":978}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index c8da847f761..31dbc09ff9d 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -103,6 +103,7 @@ import ( "github.com/prebid/prebid-server/v4/adapters/emtv" "github.com/prebid/prebid-server/v4/adapters/eplanning" "github.com/prebid/prebid-server/v4/adapters/epom" + epom_as "github.com/prebid/prebid-server/v4/adapters/epom_as" "github.com/prebid/prebid-server/v4/adapters/escalax" "github.com/prebid/prebid-server/v4/adapters/eskimi" "github.com/prebid/prebid-server/v4/adapters/exco" @@ -381,6 +382,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderEmxDigital: cadentaperturemx.Builder, openrtb_ext.BidderEPlanning: eplanning.Builder, openrtb_ext.BidderEpom: epom.Builder, + openrtb_ext.BidderEpomAs: epom_as.Builder, openrtb_ext.BidderEscalax: escalax.Builder, openrtb_ext.BidderEskimi: eskimi.Builder, openrtb_ext.BidderExco: exco.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 4858f69ef8f..4cfa74c2af8 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -119,6 +119,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderEmxDigital, BidderEPlanning, BidderEpom, + BidderEpomAs, BidderEscalax, BidderEskimi, BidderEVolution, @@ -504,6 +505,7 @@ const ( BidderEmxDigital BidderName = "emx_digital" BidderEPlanning BidderName = "eplanning" BidderEpom BidderName = "epom" + BidderEpomAs BidderName = "epom_as" BidderEscalax BidderName = "escalax" BidderEskimi BidderName = "eskimi" BidderExco BidderName = "exco" diff --git a/openrtb_ext/imp_epom_as.go b/openrtb_ext/imp_epom_as.go new file mode 100644 index 00000000000..8d5a4b80c01 --- /dev/null +++ b/openrtb_ext/imp_epom_as.go @@ -0,0 +1,18 @@ +package openrtb_ext + +// ExtImpEpomAs defines the contract for bidrequest.imp[i].ext.prebid.bidder.epom_as +type ExtImpEpomAs struct { + // Host is the serving host of the publisher's Epom Ad Server deployment. + Host string `json:"host"` + // PlacementKey identifies the placement within that deployment. + PlacementKey string `json:"placementKey"` + // Channel is a traffic-slice label used for targeting and reporting. + Channel string `json:"channel,omitempty"` + // CustomParams feed custom targeting and creative macros. + CustomParams map[string]interface{} `json:"customParams,omitempty"` + // BidFloor is a CPM floor applied only when the request carries no floor of + // its own, so a Price Floors module result always wins. + BidFloor float64 `json:"bidFloor,omitempty"` + // BidFloorCur is the currency of BidFloor, defaulting to USD. + BidFloorCur string `json:"bidFloorCur,omitempty"` +} diff --git a/static/bidder-info/epom_as.yaml b/static/bidder-info/epom_as.yaml new file mode 100644 index 00000000000..9f6257650b6 --- /dev/null +++ b/static/bidder-info/epom_as.yaml @@ -0,0 +1,25 @@ +# Epom is white-label: every publisher network serves from its own domain, so the +# host arrives per impression in imp.ext.bidder.host and is templated into the +# endpoint rather than configured here. +endpoint: "https://{{.Host}}/hb/bid" +maintainer: + email: "support@epom.com" +gvlVendorID: 849 +geoscope: + - global +openrtb: + version: 2.6 +modifyingVastXmlAllowed: false +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner +userSync: + # epom_as supports user syncing, but the sync endpoint lives on the publisher's own + # Epom deployment, so it requires configuration by the host. Contact this bidder + # directly at the email address in this file to ask about enabling user sync. + supports: + - iframe diff --git a/static/bidder-params/epom_as.json b/static/bidder-params/epom_as.json new file mode 100644 index 00000000000..ad129e91cfc --- /dev/null +++ b/static/bidder-params/epom_as.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Epom Ad Server Adapter Params", + "description": "A schema which validates params accepted by the Epom Ad Server adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Serving host of the publisher's Epom Ad Server deployment, as a bare hostname with an optional port", + "pattern": "^[a-zA-Z0-9.-]+(:[0-9]+)?$" + }, + "placementKey": { + "type": "string", + "description": "Placement identifier, from the placement's invocation-code tab in the Epom UI", + "minLength": 1 + }, + "channel": { + "type": "string", + "description": "Epom channel: publisher traffic-slice label used for channel targeting, reporting and tracking-URL propagation. Sent as imp.ext.epom_as.channel. An empty value is ignored." + }, + "customParams": { + "type": "object", + "description": "Epom custom parameters for custom targeting and creative macros. Merged into imp.ext.data, where keys already on the impression win.", + "additionalProperties": { + "type": [ + "string", + "number", + "boolean" + ] + } + }, + "bidFloor": { + "type": "number", + "description": "CPM floor for this impression, applied only when no floor has already been resolved on imp.bidfloor. 0 means no floor.", + "minimum": 0 + }, + "bidFloorCur": { + "type": "string", + "description": "Currency of bidFloor as an ISO-4217 code. Defaults to USD." + } + }, + "required": [ + "host", + "placementKey" + ] +}