diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go
index 89492eb7e06..a19d7856cea 100644
--- a/adapters/adgeneration/adgeneration.go
+++ b/adapters/adgeneration/adgeneration.go
@@ -1,12 +1,12 @@
package adgeneration
import (
+ "encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
- "strconv"
"strings"
"github.com/prebid/openrtb/v20/openrtb2"
@@ -15,41 +15,71 @@ import (
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
+ "github.com/prebid/prebid-server/v4/version"
)
-type AdgenerationAdapter struct {
+// To keep the request/response format in parity with Prebid.js v1.6.6
+// (modules/adgenerationBidAdapter.js), this adapter targets the /adgen/prebid
+// endpoint (POST with a JSON body). Only id / posall / sdktype are sent as URL
+// query parameters; everything else travels in the ortb body.
+
+type adapter struct {
endpoint string
version string
defaultCurrency string
}
-// Server Responses
+// adgRequestBody is the JSON structure of the POST body. It mirrors the
+// Prebid.js `data` object (currency / pbver / sdkname / adapterver / ortb / imark).
+type adgRequestBody struct {
+ Currency string `json:"currency"`
+ Pbver string `json:"pbver"`
+ Sdkname string `json:"sdkname"`
+ Adapterver string `json:"adapterver"`
+ Ortb openrtb2.BidRequest `json:"ortb"`
+ // imark is set to 1 only for non-native (i.e. banner) requests. This mirrors
+ // the Prebid.js adapter, whose comment notes it must be revisited if support
+ // for other media types such as video is added.
+ Imark int `json:"imark,omitempty"`
+}
+
+// adgServerResponse is the response format from the backend
+// (d.socdm.com/adgen/prebid). Prebid.js reads body.results[0], so results is
+// treated as the primary source.
type adgServerResponse struct {
- Locationid string `json:"locationid"`
- Dealid string `json:"dealid"`
- Ad string `json:"ad"`
- Beacon string `json:"beacon"`
- Beaconurl string `json:"beaconurl"`
- Cpm float64 `jsons:"cpm"`
- Creativeid string `json:"creativeid"`
- H uint64 `json:"h"`
- W uint64 `json:"w"`
- Ttl uint64 `json:"ttl"`
- Vastxml string `json:"vastxml,omitempty"`
- LandingUrl string `json:"landing_url"`
- Scheduleid string `json:"scheduleid"`
- Results []interface{} `json:"results"`
+ Locationid string `json:"locationid"`
+ LocationParams *adgLocationParams `json:"location_params,omitempty"`
+ Results []adgResult `json:"results"`
}
-func (adg *AdgenerationAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
- numRequests := len(request.Imp)
- var errs []error
+type adgLocationParams struct {
+ Option *adgLocationOption `json:"option,omitempty"`
+}
+
+type adgLocationOption struct {
+ AdType string `json:"ad_type,omitempty"`
+}
- if numRequests == 0 {
- errs = append(errs, &errortypes.BadInput{
- Message: "No impression in the bid request",
- })
- return nil, errs
+type adgResult struct {
+ Ad string `json:"ad"`
+ Beacon string `json:"beacon"`
+ Beaconurl string `json:"beaconurl"`
+ Cpm float64 `json:"cpm"`
+ Creativeid string `json:"creativeid"`
+ Dealid string `json:"dealid"`
+ H uint64 `json:"h"`
+ W uint64 `json:"w"`
+ Ttl uint64 `json:"ttl"`
+ Vastxml string `json:"vastxml,omitempty"`
+ LandingUrl string `json:"landing_url"`
+ Scheduleid string `json:"scheduleid"`
+ Adomain []string `json:"adomain,omitempty"`
+ Native json.RawMessage `json:"native,omitempty"`
+}
+
+func (adg *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ if len(request.Imp) == 0 {
+ return nil, []error{&errortypes.BadInput{Message: "No impression in the bid request"}}
}
headers := http.Header{}
@@ -64,86 +94,140 @@ func (adg *AdgenerationAdapter) MakeRequests(request *openrtb2.BidRequest, reqIn
}
}
- bidRequestArray := make([]*adapters.RequestData, 0, numRequests)
+ bidRequestArray := make([]*adapters.RequestData, 0, len(request.Imp))
+ var errs []error
- for index := 0; index < numRequests; index++ {
- bidRequestUri, err := adg.getRequestUri(request, index)
+ // Prebid.js issues one request per imp; Prebid Server does the same.
+ for index := range request.Imp {
+ req, err := adg.buildRequest(request, index, headers)
if err != nil {
errs = append(errs, err)
- return nil, errs
+ continue
}
- bidRequest := &adapters.RequestData{
- Method: "GET",
- Uri: bidRequestUri,
- Body: nil,
- Headers: headers,
- ImpIDs: []string{request.Imp[index].ID},
- }
- bidRequestArray = append(bidRequestArray, bidRequest)
+ bidRequestArray = append(bidRequestArray, req)
}
return bidRequestArray, errs
}
-func (adg *AdgenerationAdapter) getRequestUri(request *openrtb2.BidRequest, index int) (string, error) {
+func (adg *adapter) buildRequest(request *openrtb2.BidRequest, index int, headers http.Header) (*adapters.RequestData, error) {
imp := request.Imp[index]
adgExt, err := unmarshalExtImpAdgeneration(&imp)
if err != nil {
- return "", &errortypes.BadInput{
- Message: err.Error(),
- }
+ return nil, &errortypes.BadInput{Message: err.Error()}
}
- uriObj, err := url.Parse(adg.endpoint)
+
+ uri, err := adg.buildUri(adgExt.Id, request)
if err != nil {
- return "", &errortypes.BadInput{
- Message: err.Error(),
- }
+ return nil, &errortypes.BadInput{Message: err.Error()}
}
- v := adg.getRawQuery(adgExt.Id, request, &imp)
- uriObj.RawQuery = v.Encode()
- return uriObj.String(), err
+
+ body, err := adg.buildBody(request, imp)
+ if err != nil {
+ return nil, err
+ }
+
+ return &adapters.RequestData{
+ Method: http.MethodPost,
+ Uri: uri,
+ Body: body,
+ Headers: headers,
+ ImpIDs: []string{imp.ID},
+ }, nil
}
-func (adg *AdgenerationAdapter) getRawQuery(id string, request *openrtb2.BidRequest, imp *openrtb2.Imp) *url.Values {
+func (adg *adapter) buildUri(id string, request *openrtb2.BidRequest) (string, error) {
+ uriObj, err := url.Parse(adg.endpoint)
+ if err != nil {
+ return "", err
+ }
v := url.Values{}
- v.Set("posall", "SSPLOC")
v.Set("id", id)
- v.Set("hb", "true")
- v.Set("t", "json3")
- v.Set("currency", adg.getCurrency(request))
- v.Set("sdkname", "prebidserver")
- v.Set("adapterver", adg.version)
- adSize := getSizes(imp)
- if adSize != "" {
- v.Set("sizes", adSize)
- }
- if request.Device != nil && request.Device.OS == "android" {
- v.Set("sdktype", "1")
- } else if request.Device != nil && request.Device.OS == "ios" {
- v.Set("sdktype", "2")
- } else {
- v.Set("sdktype", "0")
+ v.Set("posall", "SSPLOC")
+ v.Set("sdktype", detectSdkType(request))
+ uriObj.RawQuery = v.Encode()
+ return uriObj.String(), nil
+}
+
+// detectSdkType derives the sdktype from the request origin (channel) and
+// device.os. The backend `/adgen/prebid` switches its delivery logic on
+// sdktype, so web traffic gets "0", Prebid Mobile (Android) gets "1", and
+// Prebid Mobile (iOS) gets "2". Prebid.js (client-side header bidding) always
+// sends "0", but PBS is reached through multiple paths (PBJS+PBS, or PBS-only
+// via a Mobile SDK / AMP), so it must detect the origin.
+//
+// Resolution order:
+// 1. ext.prebid.channel.name == "app" -> mobile SDK
+// 2. no channel: fall back to the presence of BidRequest.App (App means mobile)
+// 3. otherwise -> web (sdktype "0")
+//
+// When 1 or 2 matches, device.os selects 1/2; an unknown OS yields "0".
+func detectSdkType(request *openrtb2.BidRequest) string {
+ if !isAppContext(request) {
+ return "0"
}
- if request.Site != nil && request.Site.Page != "" {
- v.Set("tp", request.Site.Page)
+ if request.Device != nil {
+ switch strings.ToLower(request.Device.OS) {
+ case "android":
+ return "1"
+ case "ios":
+ return "2"
+ }
}
- if request.Source != nil && request.Source.TID != "" {
- v.Set("transactionid", request.Source.TID)
+ return "0"
+}
+
+func isAppContext(request *openrtb2.BidRequest) bool {
+ if name := requestChannelName(request); name != "" {
+ return strings.EqualFold(name, "app")
}
- if request.App != nil && request.App.Bundle != "" {
- v.Set("appbundle", request.App.Bundle)
+ // Fallback when no channel is present: treat it as a mobile app if
+ // BidRequest.App is set. (AMP / web rarely populate App, whereas the Prebid
+ // Mobile SDK does.)
+ return request.App != nil
+}
+
+func requestChannelName(request *openrtb2.BidRequest) string {
+ if request == nil || len(request.Ext) == 0 {
+ return ""
}
- if request.App != nil && request.App.Name != "" {
- v.Set("appname", request.App.Name)
+ var reqExt openrtb_ext.ExtRequest
+ if err := jsonutil.Unmarshal(request.Ext, &reqExt); err != nil {
+ return ""
}
- if request.Device != nil && request.Device.OS == "ios" && request.Device.IFA != "" {
- v.Set("idfa", request.Device.IFA)
+ if reqExt.Prebid.Channel == nil {
+ return ""
+ }
+ return reqExt.Prebid.Channel.Name
+}
+
+func (adg *adapter) buildBody(request *openrtb2.BidRequest, imp openrtb2.Imp) ([]byte, error) {
+ // ortb carries a BidRequest reduced to a single imp (same as Prebid.js). The
+ // other fields of the original request (site/app/device/user/source/regs/ext,
+ // etc.) are preserved as-is so that FPD/UserID/schain/SUA and the like reach
+ // the backend naturally.
+ ortbReq := *request
+ ortbReq.Imp = []openrtb2.Imp{imp}
+
+ pbver := version.Ver
+ if pbver == "" {
+ pbver = version.VerUnknown
+ }
+
+ body := adgRequestBody{
+ Currency: adg.getCurrency(request),
+ Pbver: pbver,
+ Sdkname: "prebidserver",
+ Adapterver: adg.version,
+ Ortb: ortbReq,
}
- if request.Device != nil && request.Device.OS == "android" && request.Device.IFA != "" {
- v.Set("advertising_id", request.Device.IFA)
+ // imark: set to 1 for non-native (assumed banner) requests. This flag
+ // originates from Prebid.js; its exact meaning on the backend is unverified.
+ if imp.Native == nil {
+ body.Imark = 1
}
- return &v
+ return json.Marshal(body)
}
func unmarshalExtImpAdgeneration(imp *openrtb2.Imp) (*openrtb_ext.ExtImpAdgeneration, error) {
@@ -161,34 +245,20 @@ func unmarshalExtImpAdgeneration(imp *openrtb2.Imp) (*openrtb_ext.ExtImpAdgenera
return &adgExt, nil
}
-func getSizes(imp *openrtb2.Imp) string {
- if imp.Banner == nil || len(imp.Banner.Format) == 0 {
- return ""
- }
- var sizeStr string
- for _, v := range imp.Banner.Format {
- sizeStr += strconv.FormatInt(v.W, 10) + "x" + strconv.FormatInt(v.H, 10) + ","
- }
- if len(sizeStr) > 0 && strings.LastIndex(sizeStr, ",") == len(sizeStr)-1 {
- sizeStr = sizeStr[:len(sizeStr)-1]
- }
- return sizeStr
-}
-
-func (adg *AdgenerationAdapter) getCurrency(request *openrtb2.BidRequest) string {
- if len(request.Cur) <= 0 {
- return adg.defaultCurrency
- } else {
- for _, c := range request.Cur {
- if adg.defaultCurrency == c {
- return c
- }
+// getCurrency follows the same either/or logic as Prebid.js
+// (adgenerationBidAdapter.js: getCurrencyType): return "USD" if request.Cur
+// contains USD, otherwise "JPY". Falling back to the first listed currency is
+// intentionally not supported (passing EUR/GBP etc. through is out of spec).
+func (adg *adapter) getCurrency(request *openrtb2.BidRequest) string {
+ for _, c := range request.Cur {
+ if strings.EqualFold(c, "USD") {
+ return "USD"
}
- return request.Cur[0]
}
+ return adg.defaultCurrency
}
-func (adg *AdgenerationAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (adg *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if response.StatusCode == http.StatusNoContent {
return nil, nil
}
@@ -202,70 +272,251 @@ func (adg *AdgenerationAdapter) MakeBids(internalRequest *openrtb2.BidRequest, e
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
}}
}
+
var bidResp adgServerResponse
- err := jsonutil.Unmarshal(response.Body, &bidResp)
- if err != nil {
+ if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil {
return nil, []error{err}
}
- if len(bidResp.Results) <= 0 {
+ if len(bidResp.Results) == 0 {
return nil, nil
}
+ // Like Prebid.js, only results[0] is used (one imp per request).
+ adResult := bidResp.Results[0]
+
+ // Prebid.js references bidRequests.data.ortb.imp[0] directly, so we do the
+ // same: take imp[0].id from the sent body and look up the matching imp. This
+ // avoids a silent no-bid when the backend omits locationid or returns a
+ // mismatched value.
+ if externalRequest == nil || len(externalRequest.Body) == 0 {
+ return nil, nil
+ }
+ var sentBody adgRequestBody
+ if err := jsonutil.Unmarshal(externalRequest.Body, &sentBody); err != nil {
+ return nil, []error{err}
+ }
+ if len(sentBody.Ortb.Imp) == 0 {
+ return nil, nil
+ }
+ targetImpID := sentBody.Ortb.Imp[0].ID
+ var matchedImp *openrtb2.Imp
+ for i := range internalRequest.Imp {
+ if internalRequest.Imp[i].ID == targetImpID {
+ matchedImp = &internalRequest.Imp[i]
+ break
+ }
+ }
+ if matchedImp == nil {
+ return nil, nil
+ }
+
+ bidType, adm, err := buildAdMarkup(&adResult, bidResp.LocationParams, matchedImp)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ bid := openrtb2.Bid{
+ ID: bidResp.Locationid,
+ ImpID: matchedImp.ID,
+ AdM: adm,
+ Price: adResult.Cpm,
+ W: int64(adResult.W),
+ H: int64(adResult.H),
+ CrID: adResult.Creativeid,
+ DealID: adResult.Dealid,
+ }
+ if len(adResult.Adomain) > 0 {
+ bid.ADomain = adResult.Adomain
+ }
+
bidResponse := adapters.NewBidderResponseWithBidsCapacity(1)
- var impId string
- var bitType openrtb_ext.BidType
- var adm string
- for _, v := range internalRequest.Imp {
- adgExt, err := unmarshalExtImpAdgeneration(&v)
+ bidResponse.Currency = adg.getCurrency(internalRequest)
+ bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+ Bid: &bid,
+ BidType: bidType,
+ })
+ return bidResponse, nil
+}
+
+// buildAdMarkup builds the AdM from results[0]. A native response takes
+// precedence; otherwise it is returned as a banner (injecting a video tag when
+// vastxml is present).
+func buildAdMarkup(adResult *adgResult, locationParams *adgLocationParams, imp *openrtb2.Imp) (openrtb_ext.BidType, string, error) {
+ // Native: assumes the native object returned by the backend is compatible
+ // with an OpenRTB native response ({"native": {...assets, link,
+ // imptrackers...}}). Like Prebid.js (isNative), it is treated as native only
+ // when assets is non-empty.
+ if len(adResult.Native) > 0 && imp.Native != nil && hasNativeAssets(adResult.Native) {
+ // AdM is the JSON string of the OpenRTB native admarkup. Whether the
+ // backend returns {"native": {...}} or the assets at the top level, it is
+ // normalized to {"native":{...}} and beaconurl is appended to imptrackers
+ // (matching Prebid.js createNativeAd, which pushes beaconurl onto
+ // impressionTrackers).
+ admBytes, err := wrapNativeAdm(adResult.Native, adResult.Beaconurl)
if err != nil {
- return nil, []error{&errortypes.BadServerResponse{
- Message: err.Error(),
- },
+ return "", "", err
+ }
+ return openrtb_ext.BidTypeNative, string(admBytes), nil
+ }
+
+ // Banner / Video-in-Banner
+ ad := adResult.Ad
+ if adResult.Vastxml != "" {
+ // Prebid.js injects the ADGBrowserM tag when
+ // location_params.option.ad_type === "upper_billboard"; otherwise it uses
+ // the APV tag.
+ if isUpperBillboard(locationParams) {
+ ad = wrapWithADGBrowserM(adResult.Vastxml, extractMarginTop(imp))
+ } else {
+ ad = wrapWithAPV(imp.ID, adResult.Vastxml)
+ }
+ }
+ ad = appendChildToBody(ad, adResult.Beacon)
+ if unwrapped := removeWrapper(ad); unwrapped != "" {
+ ad = unwrapped
+ }
+ return openrtb_ext.BidTypeBanner, ad, nil
+}
+
+// hasNativeAssets reports whether the raw JSON of results[0].native contains at
+// least one entry in assets[]. This matches Prebid.js isNative()
+// (adResult.native.assets.length > 0) and accepts both the {"native":{...}} and
+// top-level {assets:...} shapes.
+func hasNativeAssets(raw json.RawMessage) bool {
+ var top map[string]json.RawMessage
+ if err := jsonutil.Unmarshal(raw, &top); err != nil {
+ return false
+ }
+ var assets json.RawMessage
+ if inner, ok := top["native"]; ok {
+ var nat map[string]json.RawMessage
+ if err := jsonutil.Unmarshal(inner, &nat); err != nil {
+ return false
+ }
+ assets = nat["assets"]
+ } else {
+ assets = top["assets"]
+ }
+ if len(assets) == 0 {
+ return false
+ }
+ var arr []json.RawMessage
+ if err := jsonutil.Unmarshal(assets, &arr); err != nil {
+ return false
+ }
+ return len(arr) > 0
+}
+
+// wrapNativeAdm wraps the raw JSON of results[0].native for use as AdM and
+// appends beaconUrl to native.imptrackers. It absorbs both the case where the
+// backend already returns {"native":{...}} and the case where it returns the
+// assets at the top level.
+func wrapNativeAdm(raw json.RawMessage, beaconUrl string) ([]byte, error) {
+ var top map[string]json.RawMessage
+ if err := jsonutil.Unmarshal(raw, &top); err != nil {
+ return nil, err
+ }
+ var native map[string]json.RawMessage
+ if inner, ok := top["native"]; ok {
+ if err := jsonutil.Unmarshal(inner, &native); err != nil {
+ return nil, err
+ }
+ } else {
+ native = top
+ }
+
+ if beaconUrl != "" {
+ var trackers []string
+ if rawTrackers, ok := native["imptrackers"]; ok {
+ if err := jsonutil.Unmarshal(rawTrackers, &trackers); err != nil {
+ return nil, err
}
}
- if adgExt.Id == bidResp.Locationid {
- impId = v.ID
- bitType = openrtb_ext.BidTypeBanner
- adm = createAd(&bidResp, impId)
- bid := openrtb2.Bid{
- ID: bidResp.Locationid,
- ImpID: impId,
- AdM: adm,
- Price: bidResp.Cpm,
- W: int64(bidResp.W),
- H: int64(bidResp.H),
- CrID: bidResp.Creativeid,
- DealID: bidResp.Dealid,
+ duplicate := false
+ for _, t := range trackers {
+ if t == beaconUrl {
+ duplicate = true
+ break
}
-
- bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
- Bid: &bid,
- BidType: bitType,
- })
- bidResponse.Currency = adg.getCurrency(internalRequest)
- return bidResponse, nil
}
+ if !duplicate {
+ trackers = append(trackers, beaconUrl)
+ encoded, err := json.Marshal(trackers)
+ if err != nil {
+ return nil, err
+ }
+ native["imptrackers"] = encoded
+ }
+ }
+
+ nativeBytes, err := json.Marshal(native)
+ if err != nil {
+ return nil, err
}
- return nil, nil
+ return []byte(`{"native":` + string(nativeBytes) + `}`), nil
}
-func createAd(body *adgServerResponse, impId string) string {
- ad := body.Ad
- if body.Vastxml != "" {
- ad = "
" + insertVASTMethod(impId, body.Vastxml) + ""
+func isUpperBillboard(p *adgLocationParams) bool {
+ if p == nil || p.Option == nil {
+ return false
}
- ad = appendChildToBody(ad, body.Beacon)
- unwrappedAd := removeWrapper(ad)
- if unwrappedAd != "" {
- return unwrappedAd
+ return p.Option.AdType == "upper_billboard"
+}
+
+// encodeVastForJS percent-encodes VAST XML (every non-unreserved byte becomes
+// %XX) so that the JS side can restore it with decodeURIComponent. If the adm
+// contains a raw "= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
+ c == '-' || c == '_' || c == '.' || c == '~' {
+ b.WriteByte(c)
+ } else {
+ fmt.Fprintf(&b, "%%%02X", c)
+ }
+ }
+ return b.String()
+}
+
+func wrapWithAPV(impID, vastxml string) string {
+ rep := regexp.MustCompile(`\r?\n`)
+ replaced := rep.ReplaceAllString(vastxml, "")
+ return "" +
+ "" +
+ "" +
+ ""
+}
+
+func wrapWithADGBrowserM(vastxml, marginTop string) string {
+ // Prebid.js passes bidder params.marginTop to ADGBrowserM.init({marginTop}).
+ // In Prebid Server it lives at imp.ext.bidder.marginTop
+ // (ExtImpAdgeneration.MarginTop). When unset it defaults to '0', same as
+ // Prebid.js.
+ if marginTop == "" {
+ marginTop = "0"
}
- return ad
+ rep := regexp.MustCompile(`\r?\n`)
+ replaced := rep.ReplaceAllString(vastxml, "")
+ return "" +
+ "" +
+ "" +
+ ""
}
-func insertVASTMethod(bidId string, vastxml string) string {
- rep := regexp.MustCompile(`/\r?\n/g`)
- var replacedVastxml = rep.ReplaceAllString(vastxml, "")
- return ""
+// extractMarginTop extracts imp.ext.bidder.marginTop. It returns an empty string on failure.
+func extractMarginTop(imp *openrtb2.Imp) string {
+ if imp == nil || len(imp.Ext) == 0 {
+ return ""
+ }
+ adgExt, err := unmarshalExtImpAdgeneration(imp)
+ if err != nil {
+ return ""
+ }
+ return adgExt.MarginTop
}
func appendChildToBody(ad string, data string) string {
@@ -279,16 +530,16 @@ func removeWrapper(ad string) string {
if bodyIndex == -1 || lastBodyIndex == -1 {
return ""
}
-
str := strings.TrimSpace(strings.Replace(strings.Replace(ad[bodyIndex:lastBodyIndex], "", "", 1), "", "", 1))
return str
}
// Builder builds a new instance of the Adgeneration 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 := &AdgenerationAdapter{
+ bidder := &adapter{
config.Endpoint,
- "1.0.3",
+ // Aligned with Prebid.js v1.6.6 (ADGENE_PREBID_VERSION); managed as the shared ADG protocol version.
+ "1.6.6",
"JPY",
}
return bidder, nil
diff --git a/adapters/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go
index 14161750c7a..68db2a32303 100644
--- a/adapters/adgeneration/adgeneration_test.go
+++ b/adapters/adgeneration/adgeneration_test.go
@@ -2,272 +2,661 @@ package adgeneration
import (
"encoding/json"
+ "net/http"
+ "net/url"
+ "strings"
"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"
)
-func TestJsonSamples(t *testing.T) {
- bidder, buildErr := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{
- Endpoint: "https://d.socdm.com/adsv/v1"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+const testEndpoint = "https://d.socdm.com/adgen/prebid"
- if buildErr != nil {
- t.Fatalf("Builder returned unexpected error %v", buildErr)
+func newTestAdapter(t *testing.T) *adapter {
+ t.Helper()
+ bidder, err := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{Endpoint: testEndpoint},
+ config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+ if err != nil {
+ t.Fatalf("Builder returned unexpected error: %v", err)
}
-
- adapterstest.RunJSONBidderTest(t, "adgenerationtest", bidder)
+ return bidder.(*adapter)
}
-func TestGetRequestUri(t *testing.T) {
- bidder, buildErr := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{
- Endpoint: "https://d.socdm.com/adsv/v1"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
-
- if buildErr != nil {
- t.Fatalf("Builder returned unexpected error %v", buildErr)
+func TestJsonSamples(t *testing.T) {
+ bidder, err := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{Endpoint: testEndpoint},
+ config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+ if err != nil {
+ t.Fatalf("Builder returned unexpected error: %v", err)
}
+ adapterstest.RunJSONBidderTest(t, "adgenerationtest", bidder)
+}
- bidderAdgeneration, _ := bidder.(*AdgenerationAdapter)
-
- // Test items
- failedRequest := &openrtb2.BidRequest{
- ID: "test-failed-bid-request",
+func TestBuildRequestPostsToAdgenPrebid(t *testing.T) {
+ adg := newTestAdapter(t)
+ req := &openrtb2.BidRequest{
+ ID: "test",
Imp: []openrtb2.Imp{
- {ID: "extImpBidder-failed-test", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{{ "id": "58278" }}`)},
- {ID: "extImpBidder-failed-test", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"_bidder": { "id": "58278" }}`)},
- {ID: "extImpAdgeneration-failed-test", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "_id": "58278" }}`)},
+ {
+ ID: "imp-1",
+ Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
+ Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`),
+ },
},
- Source: &openrtb2.Source{TID: "SourceTID"},
- Device: &openrtb2.Device{UA: "testUA", IP: "testIP"},
+ Source: &openrtb2.Source{TID: "src-tid"},
+ Device: &openrtb2.Device{UA: "testUA", IP: "1.2.3.4"},
Site: &openrtb2.Site{Page: "https://supership.com"},
User: &openrtb2.User{BuyerUID: "buyerID"},
}
- successRequest := &openrtb2.BidRequest{
- ID: "test-success-bid-request",
+
+ requests, errs := adg.MakeRequests(req, &adapters.ExtraRequestInfo{})
+ assert.Empty(t, errs)
+ assert.Len(t, requests, 1)
+
+ r := requests[0]
+ assert.Equal(t, http.MethodPost, r.Method)
+ assert.Equal(t, []string{"imp-1"}, r.ImpIDs)
+ assert.Equal(t, "testUA", r.Headers.Get("User-Agent"))
+ assert.Equal(t, "1.2.3.4", r.Headers.Get("X-Forwarded-For"))
+
+ parsed, err := url.Parse(r.Uri)
+ assert.NoError(t, err)
+ assert.Equal(t, "d.socdm.com", parsed.Host)
+ assert.Equal(t, "/adgen/prebid", parsed.Path)
+ q := parsed.Query()
+ assert.Equal(t, "58278", q.Get("id"))
+ assert.Equal(t, "SSPLOC", q.Get("posall"))
+ assert.Equal(t, "0", q.Get("sdktype"))
+ // Parity check: the following query params, which the old upstream sent, must not be sent.
+ for _, key := range []string{"hb", "t", "currency", "sdkname", "adapterver", "sizes", "tp", "transactionid", "appbundle", "appname", "idfa", "advertising_id"} {
+ assert.False(t, q.Has(key), "query %q should not be set", key)
+ }
+
+ var body adgRequestBody
+ assert.NoError(t, json.Unmarshal(r.Body, &body))
+ assert.Equal(t, "JPY", body.Currency)
+ assert.Equal(t, "prebidserver", body.Sdkname)
+ assert.Equal(t, "1.6.6", body.Adapterver)
+ assert.Equal(t, 1, body.Imark, "banner request should set imark=1")
+ assert.NotEmpty(t, body.Pbver)
+ assert.Len(t, body.Ortb.Imp, 1)
+ assert.Equal(t, "imp-1", body.Ortb.Imp[0].ID)
+ assert.Equal(t, "https://supership.com", body.Ortb.Site.Page)
+ assert.Equal(t, "src-tid", body.Ortb.Source.TID)
+}
+
+func TestBuildRequestForNativeOmitsImark(t *testing.T) {
+ adg := newTestAdapter(t)
+ req := &openrtb2.BidRequest{
+ ID: "test",
Imp: []openrtb2.Imp{
- {ID: "bidRequest-success-test", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)},
+ {
+ ID: "imp-native",
+ Native: &openrtb2.Native{Request: `{}`},
+ Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`),
+ },
},
- Source: &openrtb2.Source{TID: "SourceTID"},
- Device: &openrtb2.Device{UA: "testUA", IP: "testIP"},
- Site: &openrtb2.Site{Page: "https://supership.com"},
- User: &openrtb2.User{BuyerUID: "buyerID"},
}
+ requests, errs := adg.MakeRequests(req, &adapters.ExtraRequestInfo{})
+ assert.Empty(t, errs)
+ assert.Len(t, requests, 1)
- numRequests := len(failedRequest.Imp)
- for index := 0; index < numRequests; index++ {
- httpRequests, err := bidderAdgeneration.getRequestUri(failedRequest, index)
- if err == nil {
- t.Errorf("getRequestUri: %v did not throw an error", failedRequest.Imp[index])
- }
- if httpRequests != "" {
- t.Errorf("getRequestUri: %v did return Request: %s", failedRequest.Imp[index], httpRequests)
- }
- }
- numRequests = len(successRequest.Imp)
- for index := 0; index < numRequests; index++ {
- // getRawQuery Test.
- adgExt, err := unmarshalExtImpAdgeneration(&successRequest.Imp[index])
- if err != nil {
- t.Errorf("unmarshalExtImpAdgeneration: %v did throw an error: %v", successRequest.Imp[index], err)
- }
- rawQuery := bidderAdgeneration.getRawQuery(adgExt.Id, successRequest, &successRequest.Imp[index])
- expectQueries := map[string]string{
- "posall": "SSPLOC",
- "id": adgExt.Id,
- "sdktype": "0",
- "hb": "true",
- "currency": bidderAdgeneration.getCurrency(successRequest),
- "sdkname": "prebidserver",
- "adapterver": bidderAdgeneration.version,
- "sizes": getSizes(&successRequest.Imp[index]),
- "tp": successRequest.Site.Page,
- "transactionid": successRequest.Source.TID,
- }
- for key, expectedValue := range expectQueries {
- actualValue := rawQuery.Get(key)
- if actualValue != expectedValue {
- t.Errorf("getRawQuery: %s value does not match expected %s, actual %s", key, expectedValue, actualValue)
- }
- }
+ var body adgRequestBody
+ assert.NoError(t, json.Unmarshal(requests[0].Body, &body))
+ assert.Equal(t, 0, body.Imark, "native request must not set imark")
+}
- // RequestUri Test.
- actualUri, err := bidderAdgeneration.getRequestUri(successRequest, index)
- if err != nil {
- t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err)
- }
- expectedUri := "https://d.socdm.com/adsv/v1?adapterver=" + bidderAdgeneration.version + "¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=https%3A%2F%2Fsupership.com&transactionid=SourceTID"
- if actualUri != expectedUri {
- t.Errorf("getRequestUri: does not match expected %s, actual %s", expectedUri, actualUri)
- }
+func TestBuildRequestRejectsBadExt(t *testing.T) {
+ adg := newTestAdapter(t)
+ req := &openrtb2.BidRequest{
+ ID: "test",
+ Imp: []openrtb2.Imp{
+ {ID: "imp-bad", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder":{"_id":"58278"}}`)},
+ {ID: "imp-ok", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`)},
+ },
}
+ requests, errs := adg.MakeRequests(req, &adapters.ExtraRequestInfo{})
+ assert.Len(t, errs, 1)
+ assert.Len(t, requests, 1, "valid imp should still produce a request")
}
-func TestGetSizes(t *testing.T) {
- // Test items
- var request *openrtb2.Imp
- var size string
- multiFormatBanner := &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 320, H: 50}}}
- noFormatBanner := &openrtb2.Banner{Format: []openrtb2.Format{}}
- nativeFormat := &openrtb2.Native{}
-
- request = &openrtb2.Imp{Banner: multiFormatBanner}
- size = getSizes(request)
- if size != "300x250,320x50" {
- t.Errorf("%v does not match size.", multiFormatBanner)
- }
- request = &openrtb2.Imp{Banner: noFormatBanner}
- size = getSizes(request)
- if size != "" {
- t.Errorf("%v does not match size.", noFormatBanner)
+// TestDetectSdkType covers deriving sdktype from channel + device.os, on the
+// assumption that the backend `/adgen/prebid` switches delivery logic on sdktype.
+func TestDetectSdkType(t *testing.T) {
+ cases := []struct {
+ name string
+ req *openrtb2.BidRequest
+ want string
+ }{
+ {
+ name: "web: channel=pbjs / site only",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"pbjs","version":"9"}}}`),
+ Site: &openrtb2.Site{Page: "https://example.com/"},
+ },
+ want: "0",
+ },
+ {
+ name: "web: channel=amp",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"amp"}}}`),
+ Site: &openrtb2.Site{Page: "https://example.com/"},
+ },
+ want: "0",
+ },
+ {
+ name: "mobile app android via channel",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"app"}}}`),
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Device: &openrtb2.Device{OS: "android"},
+ },
+ want: "1",
+ },
+ {
+ name: "mobile app ios via channel (case insensitive)",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"APP"}}}`),
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Device: &openrtb2.Device{OS: "iOS"},
+ },
+ want: "2",
+ },
+ {
+ name: "fallback: no channel, BidRequest.App only (android)",
+ req: &openrtb2.BidRequest{
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Device: &openrtb2.Device{OS: "android"},
+ },
+ want: "1",
+ },
+ {
+ name: "fallback: no channel, BidRequest.App only (ios)",
+ req: &openrtb2.BidRequest{
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Device: &openrtb2.Device{OS: "ios"},
+ },
+ want: "2",
+ },
+ {
+ name: "app context but unknown device.os -> 0",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"app"}}}`),
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Device: &openrtb2.Device{OS: "tvos"},
+ },
+ want: "0",
+ },
+ {
+ name: "both app and site nil -> treated as web",
+ req: &openrtb2.BidRequest{},
+ want: "0",
+ },
+ {
+ name: "channel wins when both app and site present (channel=app)",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{"channel":{"name":"app"}}}`),
+ App: &openrtb2.App{Bundle: "com.example.app"},
+ Site: &openrtb2.Site{Page: "https://example.com/"},
+ Device: &openrtb2.Device{OS: "android"},
+ },
+ want: "1",
+ },
+ {
+ name: "malformed ext is treated as no channel (= web when site present)",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{not-json`),
+ Site: &openrtb2.Site{Page: "https://example.com/"},
+ },
+ want: "0",
+ },
+ {
+ name: "ext.prebid present but no channel (= web when site present)",
+ req: &openrtb2.BidRequest{
+ Ext: json.RawMessage(`{"prebid":{}}`),
+ Site: &openrtb2.Site{Page: "https://example.com/"},
+ },
+ want: "0",
+ },
}
- request = &openrtb2.Imp{Native: nativeFormat}
- size = getSizes(request)
- if size != "" {
- t.Errorf("%v does not match size.", nativeFormat)
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ assert.Equal(t, c.want, detectSdkType(c.req))
+ })
}
}
+// TestGetCurrency covers the same either/or behavior as Prebid.js
+// (adgenerationBidAdapter.js: getCurrencyType): USD if USD is present, otherwise JPY.
func TestGetCurrency(t *testing.T) {
- bidder, buildErr := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{
- Endpoint: "https://d.socdm.com/adsv/v1"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
-
- if buildErr != nil {
- t.Fatalf("Builder returned unexpected error %v", buildErr)
+ adg := newTestAdapter(t)
+ cases := []struct {
+ name string
+ cur []string
+ want string
+ }{
+ {"default JPY when empty", nil, "JPY"},
+ {"USD wins over JPY", []string{"USD", "JPY"}, "USD"},
+ {"USD only", []string{"USD"}, "USD"},
+ {"unrelated currency falls back to JPY", []string{"EUR"}, "JPY"},
+ {"JPY only", []string{"JPY"}, "JPY"},
+ {"case-insensitive usd", []string{"usd"}, "USD"},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ got := adg.getCurrency(&openrtb2.BidRequest{Cur: c.cur})
+ assert.Equal(t, c.want, got)
+ })
}
+}
- bidderAdgeneration, _ := bidder.(*AdgenerationAdapter)
+func TestBuildAdMarkupBanner(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Beacon: "
",
+ Beaconurl: "https://b.example/",
+ Cpm: 50,
+ }
+ imp := &openrtb2.Imp{ID: "imp-1", Banner: &openrtb2.Banner{}}
- // Test items
- var request *openrtb2.BidRequest
- var currency string
- innerDefaultCur := []string{"USD", "JPY"}
- usdCur := []string{"USD", "EUR"}
+ bidType, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+ assert.Equal(t, "
", adm)
+}
- request = &openrtb2.BidRequest{Cur: innerDefaultCur}
- currency = bidderAdgeneration.getCurrency(request)
- if currency != "JPY" {
- t.Errorf("%v does not match currency.", innerDefaultCur)
+func TestBuildAdMarkupVastUsesAPV(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Beacon: "
",
+ Vastxml: "",
}
- request = &openrtb2.BidRequest{Cur: usdCur}
- currency = bidderAdgeneration.getCurrency(request)
- if currency != "USD" {
- t.Errorf("%v does not match currency.", usdCur)
+ imp := &openrtb2.Imp{ID: "imp-vast", Banner: &openrtb2.Banner{}}
+ bidType, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+ assert.Contains(t, adm, "apvad-imp-vast")
+ assert.Contains(t, adm, "cdn.apvdr.com/js/VideoAd.min.js")
+}
+
+// Newlines contained in vastxml must not remain inside the JS string literal (equivalent to Prebid.js: /\r?\n/g).
+func TestBuildAdMarkupVastStripsNewlinesInsideJsLiteral(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Vastxml: "\r\nfoo\nbar\n",
}
+ imp := &openrtb2.Imp{ID: "imp-vast", Banner: &openrtb2.Banner{}}
+ _, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ // A newline left inside the APV.VideoAd(...).load('...') argument would break the JS string.
+ assert.NotContains(t, adm, "load('\r\nfoo")
+ assert.NotContains(t, adm, "load('\nfoo")
+ // VAST is percent-encoded and restored with decodeURIComponent
+ // (a raw "")
}
-func TestCreateAd(t *testing.T) {
- // Test items
- adgBannerImpId := "test-banner-imp"
- adgBannerResponse := adgServerResponse{
- Ad: "\n\n\n\n\n\n\n
\n\n",
- Beacon: "
",
- Beaconurl: "https://dummy-beacon.com",
- Cpm: 50,
- Creativeid: "DummyDsp_SdkTeam_supership.jp",
- H: 300,
- W: 250,
- Ttl: 10,
- LandingUrl: "",
- Scheduleid: "111111",
+func TestBuildAdMarkupADGBrowserMStripsNewlines(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Vastxml: "\r\nfoo\n",
}
- matchBannerTag := "\n\n
\n
"
-
- adgVastImpId := "test-vast-imp"
- adgVastResponse := adgServerResponse{
- Ad: "\n\n\n\n\n\n\n
\n\n",
- Beacon: "
",
- Beaconurl: "https://dummy-beacon.com",
- Cpm: 50,
- Creativeid: "DummyDsp_SdkTeam_supership.jp",
- H: 300,
- W: 250,
- Ttl: 10,
- LandingUrl: "",
- Vastxml: "",
- Scheduleid: "111111",
+ loc := &adgLocationParams{Option: &adgLocationOption{AdType: "upper_billboard"}}
+ imp := &openrtb2.Imp{ID: "imp-ub", Banner: &openrtb2.Banner{}}
+ _, adm, err := buildAdMarkup(adResult, loc, imp)
+ assert.NoError(t, err)
+ assert.NotContains(t, adm, "vastXml: '\r\nfoo")
+ // VAST is percent-encoded and restored with decodeURIComponent
+ // (a raw "")
+}
+
+func TestBuildAdMarkupVastUsesADGBrowserMOnUpperBillboard(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Beacon: "
",
+ Vastxml: "",
}
- matchVastTag := "
"
+ loc := &adgLocationParams{Option: &adgLocationOption{AdType: "upper_billboard"}}
+ imp := &openrtb2.Imp{ID: "imp-ub", Banner: &openrtb2.Banner{}}
+ bidType, adm, err := buildAdMarkup(adResult, loc, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+ assert.Contains(t, adm, "adg-browser-m.js")
+ assert.NotContains(t, adm, "apvad-")
+ // When marginTop is unset, fill in '0' just like Prebid.js.
+ assert.Contains(t, adm, "marginTop: '0'")
+}
- bannerAd := createAd(&adgBannerResponse, adgBannerImpId)
- if bannerAd != matchBannerTag {
- t.Errorf("%v does not match createAd.", adgBannerResponse)
+func TestBuildAdMarkupVastADGBrowserMUsesBidderMarginTop(t *testing.T) {
+ adResult := &adgResult{
+ Ad: "",
+ Vastxml: "",
}
- vastAd := createAd(&adgVastResponse, adgVastImpId)
- if vastAd != matchVastTag {
- t.Errorf("%v does not match createAd.", adgVastResponse)
+ loc := &adgLocationParams{Option: &adgLocationOption{AdType: "upper_billboard"}}
+ imp := &openrtb2.Imp{
+ ID: "imp-ub",
+ Banner: &openrtb2.Banner{},
+ Ext: json.RawMessage(`{"bidder":{"id":"58278","marginTop":"42"}}`),
}
+ _, adm, err := buildAdMarkup(adResult, loc, imp)
+ assert.NoError(t, err)
+ assert.Contains(t, adm, "marginTop: '42'")
}
-func TestMakeBids(t *testing.T) {
- bidder, buildErr := Builder(openrtb_ext.BidderAdgeneration, config.Adapter{
- Endpoint: "https://d.socdm.com/adsv/v1"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+func TestBuildAdMarkupNative(t *testing.T) {
+ rawNative := json.RawMessage(`{"assets":[{"id":1,"title":{"text":"hello"}}],"link":{"url":"https://l.example/"}}`)
+ adResult := &adgResult{Native: rawNative}
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+
+ bidType, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeNative, bidType)
+ assert.True(t, strings.HasPrefix(adm, `{"native":`))
+ assert.Contains(t, adm, `"assets"`)
+}
+
+func TestBuildAdMarkupNativeAppendsBeaconUrlToImptrackers(t *testing.T) {
+ rawNative := json.RawMessage(`{"assets":[{"id":1,"title":{"text":"hello"}}],"link":{"url":"https://l.example/"},"imptrackers":["https://existing.example/imp"]}`)
+ adResult := &adgResult{Native: rawNative, Beaconurl: "https://tg.example/bc"}
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+
+ _, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Contains(t, adm, "https://existing.example/imp")
+ assert.Contains(t, adm, "https://tg.example/bc", "beaconurl must be appended to imptrackers")
+}
- if buildErr != nil {
- t.Fatalf("Builder returned unexpected error %v", buildErr)
+func TestBuildAdMarkupNativeBeaconUrlDeduplicated(t *testing.T) {
+ rawNative := json.RawMessage(`{"assets":[{"id":1,"title":{"text":"hi"}}],"imptrackers":["https://tg.example/bc"]}`)
+ adResult := &adgResult{Native: rawNative, Beaconurl: "https://tg.example/bc"}
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+
+ _, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ // Do not add a duplicate when it is already present in imptrackers.
+ assert.Equal(t, 1, strings.Count(adm, "https://tg.example/bc"))
+}
+
+// Prebid.js isNative() compatible: when assets is empty/missing, treat it as banner rather than native.
+func TestBuildAdMarkupFallsBackToBannerWhenNativeAssetsMissing(t *testing.T) {
+ cases := []struct {
+ name string
+ raw string
+ }{
+ {"empty assets", `{"assets":[],"link":{"url":"https://l.example/"}}`},
+ {"no assets key", `{"link":{"url":"https://l.example/"}}`},
+ {"wrapped empty assets", `{"native":{"assets":[]}}`},
}
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ adResult := &adgResult{
+ Native: json.RawMessage(c.raw),
+ Ad: "fallback banner",
+ }
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+ bidType, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+ assert.Equal(t, "fallback banner", adm)
+ })
+ }
+}
- bidderAdgeneration, _ := bidder.(*AdgenerationAdapter)
+func TestBuildAdMarkupNativeAcceptsWrappedInput(t *testing.T) {
+ rawNative := json.RawMessage(`{"native":{"assets":[{"id":1,"title":{"text":"hi"}}]}}`)
+ adResult := &adgResult{Native: rawNative, Beaconurl: "https://tg.example/bc"}
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+
+ _, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.True(t, strings.HasPrefix(adm, `{"native":`))
+ // Do not double-wrap a nested native (= "native" appears only once in the output).
+ assert.Equal(t, 1, strings.Count(adm, `"native"`))
+ assert.Contains(t, adm, "https://tg.example/bc")
+}
+
+func TestMakeBidsReadsResultsAndAdomain(t *testing.T) {
+ adg := newTestAdapter(t)
internalRequest := &openrtb2.BidRequest{
- ID: "test-success-bid-request",
+ ID: "test",
Imp: []openrtb2.Imp{
- {ID: "bidRequest-success-test", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)},
+ {ID: "imp-1", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`)},
},
- Device: &openrtb2.Device{UA: "testUA", IP: "testIP"},
- Site: &openrtb2.Site{Page: "https://supership.com"},
- User: &openrtb2.User{BuyerUID: "buyerID"},
}
- externalRequest := adapters.RequestData{}
- response := adapters.ResponseData{
- StatusCode: 200,
- Body: ([]byte)("{\n \"ad\": \"testAd\",\n \"cpm\": 30,\n \"creativeid\": \"Dummy_supership.jp\",\n \"h\": 250,\n \"locationid\": \"58278\",\n \"results\": [{}],\n \"dealid\": \"test-deal-id\",\n \"w\": 300\n }"),
+ respBody := `{
+ "locationid": "58278",
+ "results": [{
+ "ad": "testAd",
+ "beacon": "",
+ "cpm": 30,
+ "creativeid": "Dummy_supership.jp",
+ "dealid": "test-deal",
+ "h": 250,
+ "w": 300,
+ "adomain": ["advertiser.example"]
+ }]
+ }`
+ resp := &adapters.ResponseData{StatusCode: 200, Body: []byte(respBody)}
+
+ sentBody, _ := json.Marshal(adgRequestBody{Ortb: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "imp-1"}}}})
+ bidderResp, errs := adg.MakeBids(internalRequest, &adapters.RequestData{Body: sentBody}, resp)
+ assert.Empty(t, errs)
+ assert.NotNil(t, bidderResp)
+ assert.Equal(t, "JPY", bidderResp.Currency)
+ assert.Len(t, bidderResp.Bids, 1)
+
+ bid := bidderResp.Bids[0]
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bid.BidType)
+ assert.Equal(t, "58278", bid.Bid.ID)
+ assert.Equal(t, "imp-1", bid.Bid.ImpID)
+ assert.Equal(t, "testAd", bid.Bid.AdM)
+ assert.Equal(t, 30.0, bid.Bid.Price)
+ assert.Equal(t, int64(300), bid.Bid.W)
+ assert.Equal(t, int64(250), bid.Bid.H)
+ assert.Equal(t, "Dummy_supership.jp", bid.Bid.CrID)
+ assert.Equal(t, "test-deal", bid.Bid.DealID)
+ assert.Equal(t, []string{"advertiser.example"}, bid.Bid.ADomain)
+}
+
+func TestMakeBidsReturnsNilOnNoContent(t *testing.T) {
+ adg := newTestAdapter(t)
+ resp := &adapters.ResponseData{StatusCode: http.StatusNoContent}
+ bidderResp, errs := adg.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, resp)
+ assert.Nil(t, bidderResp)
+ assert.Empty(t, errs)
+}
+
+func TestMakeBidsReturnsErrorOn400(t *testing.T) {
+ adg := newTestAdapter(t)
+ resp := &adapters.ResponseData{StatusCode: http.StatusBadRequest}
+ bidderResp, errs := adg.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, resp)
+ assert.Nil(t, bidderResp)
+ assert.Len(t, errs, 1)
+ assert.IsType(t, &errortypes.BadInput{}, errs[0])
+}
+
+func TestMakeRequestsReturnsErrorWhenNoImp(t *testing.T) {
+ adg := newTestAdapter(t)
+ requests, errs := adg.MakeRequests(&openrtb2.BidRequest{ID: "test"}, &adapters.ExtraRequestInfo{})
+ assert.Nil(t, requests)
+ assert.Len(t, errs, 1)
+ assert.IsType(t, &errortypes.BadInput{}, errs[0])
+}
+
+// Covers each error branch of unmarshalExtImpAdgeneration.
+func TestUnmarshalExtImpAdgenerationErrors(t *testing.T) {
+ cases := []struct {
+ name string
+ ext json.RawMessage
+ wantMsg string // empty means any error is acceptable
+ }{
+ {"invalid imp.ext JSON", json.RawMessage(`not-json`), ""},
+ {"bidder is not an object", json.RawMessage(`{"bidder":"not-an-object"}`), ""},
+ {"id is empty string", json.RawMessage(`{"bidder":{"id":""}}`), "No Location ID in ExtImpAdgeneration."},
+ {"id key missing", json.RawMessage(`{"bidder":{"marginTop":"10"}}`), "No Location ID in ExtImpAdgeneration."},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ imp := &openrtb2.Imp{ID: "imp-x", Ext: c.ext}
+ adgExt, err := unmarshalExtImpAdgeneration(imp)
+ assert.Nil(t, adgExt)
+ assert.Error(t, err)
+ if c.wantMsg != "" {
+ assert.Equal(t, c.wantMsg, err.Error())
+ }
+ })
}
- // default Currency InternalRequest
- defaultCurBidderResponse, errs := bidder.MakeBids(internalRequest, &externalRequest, &response)
- if len(errs) > 0 {
- t.Errorf("MakeBids return errors. errors: %v", errs)
+}
+
+// hasNativeAssets: covers wrapped/unwrapped detection plus invalid input (Prebid.js isNative compatible).
+func TestHasNativeAssets(t *testing.T) {
+ cases := []struct {
+ name string
+ raw string
+ want bool
+ }{
+ {"unwrapped with assets", `{"assets":[{"id":1}]}`, true},
+ {"wrapped with assets", `{"native":{"assets":[{"id":1}]}}`, true},
+ {"empty assets array", `{"assets":[]}`, false},
+ {"no assets key", `{"link":{"url":"https://l.example/"}}`, false},
+ {"invalid JSON", `not-json`, false},
+ {"wrapped native is not an object", `{"native":123}`, false},
+ {"assets is not an array", `{"assets":"foo"}`, false},
+ {"wrapped assets is not an array", `{"native":{"assets":"foo"}}`, false},
}
- checkBidResponse(t, defaultCurBidderResponse, bidderAdgeneration.defaultCurrency)
-
- // Specified Currency InternalRequest
- usdCur := "USD"
- internalRequest.Cur = []string{usdCur}
- specifiedCurBidderResponse, errs := bidder.MakeBids(internalRequest, &externalRequest, &response)
- if len(errs) > 0 {
- t.Errorf("MakeBids return errors. errors: %v", errs)
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ assert.Equal(t, c.want, hasNativeAssets(json.RawMessage(c.raw)))
+ })
}
- checkBidResponse(t, specifiedCurBidderResponse, usdCur)
+}
+// Covers the error branches of wrapNativeAdm (unmarshal failures for top/native/imptrackers).
+func TestWrapNativeAdmErrors(t *testing.T) {
+ // top-level unmarshal failure
+ _, err := wrapNativeAdm(json.RawMessage(`not-json`), "")
+ assert.Error(t, err)
+ // wrapped native is not an object
+ _, err = wrapNativeAdm(json.RawMessage(`{"native":123}`), "")
+ assert.Error(t, err)
+ // imptrackers is not a string array (only reached when appending beaconUrl)
+ _, err = wrapNativeAdm(json.RawMessage(`{"assets":[{"id":1}],"imptrackers":"not-array"}`), "https://b.example/bc")
+ assert.Error(t, err)
}
-func checkBidResponse(t *testing.T, bidderResponse *adapters.BidderResponse, expectedCurrency string) {
- if bidderResponse == nil {
- t.Errorf("actual bidResponse is nil.")
+// buildAdMarkup: the path where wrapNativeAdm returns an error while assembling the native adm.
+func TestBuildAdMarkupNativeWrapError(t *testing.T) {
+ adResult := &adgResult{
+ Native: json.RawMessage(`{"assets":[{"id":1}],"imptrackers":"not-array"}`),
+ Beaconurl: "https://b.example/bc",
}
+ imp := &openrtb2.Imp{ID: "imp-native", Native: &openrtb2.Native{Request: `{}`}}
+ _, _, err := buildAdMarkup(adResult, nil, imp)
+ assert.Error(t, err)
+}
+
+// removeWrapper: an ad without is returned as-is (not unwrapped).
+func TestBuildAdMarkupBannerWithoutBodyTags(t *testing.T) {
+ adResult := &adgResult{Ad: "plain-ad-no-body"}
+ imp := &openrtb2.Imp{ID: "imp-1", Banner: &openrtb2.Banner{}}
+ bidType, adm, err := buildAdMarkup(adResult, nil, imp)
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+ assert.Equal(t, "plain-ad-no-body", adm)
+}
+
+// extractMarginTop: even with a malformed imp.ext, marginTop is treated as empty (defaults to '0').
+func TestBuildAdMarkupUpperBillboardHandlesBadExt(t *testing.T) {
+ adResult := &adgResult{Ad: "", Vastxml: ""}
+ loc := &adgLocationParams{Option: &adgLocationOption{AdType: "upper_billboard"}}
+ imp := &openrtb2.Imp{ID: "imp-ub", Banner: &openrtb2.Banner{}, Ext: json.RawMessage(`not-json`)}
+ _, adm, err := buildAdMarkup(adResult, loc, imp)
+ assert.NoError(t, err)
+ assert.Contains(t, adm, "marginTop: '0'")
+}
- // AdM is assured by TestCreateAd and JSON tests
- var expectedAdM string = "testAd"
- var expectedID string = "58278"
- var expectedImpID = "bidRequest-success-test"
- var expectedPrice float64 = 30.0
- var expectedW int64 = 300
- var expectedH int64 = 250
- var expectedCrID string = "Dummy_supership.jp"
- var extectedDealID string = "test-deal-id"
-
- //nolint: staticcheck // false positive SA5011: possible nil pointer dereference
- assert.Equal(t, expectedCurrency, bidderResponse.Currency)
- assert.Equal(t, 1, len(bidderResponse.Bids))
- assert.Equal(t, expectedID, bidderResponse.Bids[0].Bid.ID)
- assert.Equal(t, expectedImpID, bidderResponse.Bids[0].Bid.ImpID)
- assert.Equal(t, expectedAdM, bidderResponse.Bids[0].Bid.AdM)
- assert.Equal(t, expectedPrice, bidderResponse.Bids[0].Bid.Price)
- assert.Equal(t, expectedW, bidderResponse.Bids[0].Bid.W)
- assert.Equal(t, expectedH, bidderResponse.Bids[0].Bid.H)
- assert.Equal(t, expectedCrID, bidderResponse.Bids[0].Bid.CrID)
- assert.Equal(t, extectedDealID, bidderResponse.Bids[0].Bid.DealID)
+// MakeBids: a 500 returns BadServerResponse.
+func TestMakeBidsReturnsServerErrorOn500(t *testing.T) {
+ adg := newTestAdapter(t)
+ resp := &adapters.ResponseData{StatusCode: http.StatusInternalServerError}
+ bidderResp, errs := adg.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, resp)
+ assert.Nil(t, bidderResp)
+ assert.Len(t, errs, 1)
+ assert.IsType(t, &errortypes.BadServerResponse{}, errs[0])
+}
+
+// MakeBids: a 200 with an invalid JSON body returns an error.
+func TestMakeBidsReturnsErrorOnInvalidBody(t *testing.T) {
+ adg := newTestAdapter(t)
+ resp := &adapters.ResponseData{StatusCode: http.StatusOK, Body: []byte(`not-json`)}
+ bidderResp, errs := adg.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{Body: []byte(`{}`)}, resp)
+ assert.Nil(t, bidderResp)
+ assert.Len(t, errs, 1)
+}
+
+// MakeBids: covers the guard branches around externalRequest / sentBody.
+func TestMakeBidsExternalRequestGuards(t *testing.T) {
+ adg := newTestAdapter(t)
+ internal := &openrtb2.BidRequest{
+ Imp: []openrtb2.Imp{{ID: "imp-1", Banner: &openrtb2.Banner{}, Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`)}},
+ }
+ goodResp := func() *adapters.ResponseData {
+ return &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: []byte(`{"locationid":"58278","results":[{"ad":"x","cpm":1}]}`),
+ }
+ }
+
+ t.Run("externalRequest is nil", func(t *testing.T) {
+ bidderResp, errs := adg.MakeBids(internal, nil, goodResp())
+ assert.Nil(t, bidderResp)
+ assert.Empty(t, errs)
+ })
+ t.Run("externalRequest.Body is empty", func(t *testing.T) {
+ bidderResp, errs := adg.MakeBids(internal, &adapters.RequestData{}, goodResp())
+ assert.Nil(t, bidderResp)
+ assert.Empty(t, errs)
+ })
+ t.Run("sentBody is invalid JSON", func(t *testing.T) {
+ bidderResp, errs := adg.MakeBids(internal, &adapters.RequestData{Body: []byte(`not-json`)}, goodResp())
+ assert.Nil(t, bidderResp)
+ assert.Len(t, errs, 1)
+ })
+ t.Run("sentBody.Ortb has no imp", func(t *testing.T) {
+ sentBody, _ := json.Marshal(adgRequestBody{})
+ bidderResp, errs := adg.MakeBids(internal, &adapters.RequestData{Body: sentBody}, goodResp())
+ assert.Nil(t, bidderResp)
+ assert.Empty(t, errs)
+ })
+ t.Run("sentBody imp ID not found in internalRequest", func(t *testing.T) {
+ sentBody, _ := json.Marshal(adgRequestBody{Ortb: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "no-such-imp"}}}})
+ bidderResp, errs := adg.MakeBids(internal, &adapters.RequestData{Body: sentBody}, goodResp())
+ assert.Nil(t, bidderResp)
+ assert.Empty(t, errs)
+ })
+}
+
+// MakeBids: returns an error when assembling the native adm fails (via buildAdMarkup).
+func TestMakeBidsReturnsErrorWhenNativeAdmWrapFails(t *testing.T) {
+ adg := newTestAdapter(t)
+ internal := &openrtb2.BidRequest{
+ Imp: []openrtb2.Imp{{ID: "imp-1", Native: &openrtb2.Native{Request: `{}`}, Ext: json.RawMessage(`{"bidder":{"id":"58278"}}`)}},
+ }
+ resp := &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: []byte(`{"locationid":"58278","results":[{"native":{"assets":[{"id":1}],"imptrackers":"not-array"},"beaconurl":"https://b.example/bc","cpm":10}]}`),
+ }
+ sentBody, _ := json.Marshal(adgRequestBody{Ortb: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "imp-1"}}}})
+ bidderResp, errs := adg.MakeBids(internal, &adapters.RequestData{Body: sentBody}, resp)
+ assert.Nil(t, bidderResp)
+ assert.Len(t, errs, 1)
}
diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner-android.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner-android.json
index 1753abc7127..aaff417599c 100644
--- a/adapters/adgeneration/adgenerationtest/exemplary/single-banner-android.json
+++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner-android.json
@@ -1,5 +1,5 @@
{
- "mockBidRequest":{
+ "mockBidRequest": {
"id": "some-request-id",
"site": {
"page": "http://example.com/test.html"
@@ -35,33 +35,49 @@
},
"httpCalls": [
{
- "internalRequest": {
- "id": "some-request-id",
- "site": {
- "page": "http://example.com/test.html"
- },
- "imp": [
- {
- "id": "some-impression-id",
- "banner": {
- "format": [
- {
- "w": 300,
- "h": 250
+ "expectedRequest": {
+ "uri": "https://d.socdm.com/adgen/prebid?id=58278&posall=SSPLOC&sdktype=1",
+ "body": {
+ "currency": "JPY",
+ "pbver": "unknown",
+ "sdkname": "prebidserver",
+ "adapterver": "1.6.6",
+ "ortb": {
+ "id": "some-request-id",
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "id": "58278"
+ }
}
- ]
- },
- "ext": {
- "bidder": {
- "id": "58278"
}
- }
- }
- ],
- "tmax": 500
- },
- "expectedRequest":{
- "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.3&advertising_id=advertising_id&appname=adgneration¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=1&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+ ],
+ "site": {
+ "page": "http://example.com/test.html"
+ },
+ "app": {
+ "name": "adgneration"
+ },
+ "device": {
+ "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
+ "ip": "0.0.0.0",
+ "os": "android",
+ "ifa": "advertising_id"
+ },
+ "tmax": 500
+ },
+ "imark": 1
+ },
"headers": {
"Accept": [
"application/json"
@@ -76,92 +92,49 @@
"0.0.0.0"
]
},
- "impIDs":["some-impression-id"]
+ "impIDs": ["some-impression-id"]
},
- "mockResponse":{
+ "mockResponse": {
"status": 200,
"body": {
- "ad": "\n \n \n