Repository: prebid/prebid-server (go)

Issue:
Support `hb_format` for multiformat AMP requests
Prebid.js supports `hb_format` to enable multiformat imps on AMP, see [multi-format](http://prebid.org/adops/setting-up-prebid-multi-format-in-dfp.html). The spec itself already exists in the prebid.js documentation, we just need to implement this on PBS.

In line with other flags we have on key value generation, I propose we add an `includeformat` flag to the `targeting` extension. So the targeting API would look like:
```
{
  "ext": {
    "prebid": {
      "targeting": {
        "pricegranularity": {
          "precision": 2,
          "ranges": [{
            "max": 20.00,
            "increment": 0.10 // This is equivalent to the deprecated "pricegranularity": "medium"
          }]
        },
        "includewinners": false, // Optional param defaulting to true
        "includebidderkeys": false // Optional param defaulting to true
        "includeformat": false //Optional param defaulting to false
      }
    }
  }
}
```
Thus all current requests would not get `hb_format` added to the key values, but requests that need it can add the flag in order to get access to this value.

Pull request description:
Adds keyvalue hb_format support
Resolves #1412 

Patch:
diff --git a/adapters/gumgum/gumgum.go b/adapters/gumgum/gumgum.go
index 84a008d1..49097909 100644
--- a/adapters/gumgum/gumgum.go
+++ b/adapters/gumgum/gumgum.go
@@ -8,12 +8,16 @@ import (
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"net/http"
+	"strconv"
+	"strings"
 )
 
+// GumGumAdapter implements Bidder interface.
 type GumGumAdapter struct {
 	URI string
 }
 
+// MakeRequests makes the HTTP requests which should be made to fetch bids.
 func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var validImps []openrtb.Imp
 	var trackingId string
@@ -26,15 +30,21 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt
 		zone, err := preprocess(&imp)
 		if err != nil {
 			errs = append(errs, err)
-		} else {
-			if request.Imp[i].Banner != nil {
-				bannerCopy := *request.Imp[i].Banner
-				if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
-					format := bannerCopy.Format[0]
-					bannerCopy.W = &(format.W)
-					bannerCopy.H = &(format.H)
-				}
-				request.Imp[i].Banner = &bannerCopy
+		} else if request.Imp[i].Banner != nil {
+			bannerCopy := *request.Imp[i].Banner
+			if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
+				format := bannerCopy.Format[0]
+				bannerCopy.W = &(format.W)
+				bannerCopy.H = &(format.H)
+			}
+			request.Imp[i].Banner = &bannerCopy
+			validImps = append(validImps, request.Imp[i])
+			trackingId = zone
+		} else if request.Imp[i].Video != nil {
+			err := validateVideoParams(request.Imp[i].Video)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
 				validImps = append(validImps, request.Imp[i])
 				trackingId = zone
 			}
@@ -70,6 +80,7 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt
 	}}, errs
 }
 
+// MakeBids unpacks the server's response into Bids.
 func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
@@ -98,12 +109,19 @@ func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRe
 
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
+			mediaType := getMediaTypeForImpID(sb.Bid[i].ImpID, internalRequest.Imp)
+			if mediaType == openrtb_ext.BidTypeVideo {
+				price := strconv.FormatFloat(sb.Bid[i].Price, 'f', -1, 64)
+				sb.Bid[i].AdM = strings.Replace(sb.Bid[i].AdM, "${AUCTION_PRICE}", price, -1)
+			}
+
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &sb.Bid[i],
-				BidType: openrtb_ext.BidTypeBanner,
+				BidType: mediaType,
 			})
 		}
 	}
+
 	return bidResponse, errs
 }
 
@@ -128,6 +146,25 @@ func preprocess(imp *openrtb.Imp) (string, error) {
 	return zone, nil
 }
 
+func getMediaTypeForImpID(impID string, imps []openrtb.Imp) openrtb_ext.BidType {
+	for _, imp := range imps {
+		if imp.ID == impID && imp.Banner != nil {
+			return openrtb_ext.BidTypeBanner
+		}
+	}
+	return openrtb_ext.BidTypeVideo
+}
+
+func validateVideoParams(video *openrtb.Video) (err error) {
+	if video.W == 0 || video.H == 0 || video.MinDuration == 0 || video.MaxDuration == 0 || video.Placement == 0 || video.Linearity == 0 {
+		return &errortypes.BadInput{
+			Message: "Invalid or missing video field(s)",
+		}
+	}
+	return nil
+}
+
+// NewGumGumBidder configures bidder endpoint.
 func NewGumGumBidder(endpoint string) *GumGumAdapter {
 	return &GumGumAdapter{
 		URI: endpoint,
diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go
index ca88b18b..c2a42adf 100644
--- a/adapters/openx/openx.go
+++ b/adapters/openx/openx.go
@@ -22,7 +22,8 @@ type openxImpExt struct {
 }
 
 type openxReqExt struct {
-	DelDomain    string `json:"delDomain"`
+	DelDomain    string `json:"delDomain,omitempty"`
+	Platform     string `json:"platform,omitempty"`
 	BidderConfig string `json:"bc"`
 }
 
@@ -125,6 +126,7 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error {
 	}
 
 	reqExt.DelDomain = openxExt.DelDomain
+	reqExt.Platform = openxExt.Platform
 
 	imp.TagID = openxExt.Unit
 	imp.BidFloor = openxExt.CustomFloor
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index aae11538..79565504 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -21,7 +21,6 @@ import (
 )
 
 const MAX_IMPRESSIONS_PUBMATIC = 30
-const bidTypeExtKey = "BidType"
 
 type PubmaticAdapter struct {
 	http *adapters.HTTPAdapter
@@ -48,6 +47,15 @@ type pubmaticParams struct {
 	Keywords    map[string]string `json:"keywords,omitempty"`
 }
 
+type pubmaticBidExtVideo struct {
+	Duration *int `json:"duration,omitempty"`
+}
+
+type pubmaticBidExt struct {
+	BidType           *int                 `json:"BidType,omitempty"`
+	VideoCreativeInfo *pubmaticBidExtVideo `json:"video,omitempty"`
+}
+
 const (
 	INVALID_PARAMS    = "Invalid BidParam"
 	MISSING_PUBID     = "Missing PubID"
@@ -289,7 +297,11 @@ func (a *PubmaticAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder
 				DealId:      bid.DealID,
 			}
 
-			mediaType := getBidType(bid.Ext)
+			var bidExt pubmaticBidExt
+			mediaType := openrtb_ext.BidTypeBanner
+			if err := json.Unmarshal(bid.Ext, &bidExt); err == nil {
+				mediaType = getBidType(&bidExt)
+			}
 			pbid.CreativeMediaType = string(mediaType)
 
 			bids = append(bids, &pbid)
@@ -549,9 +561,24 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 	for _, sb := range bidResp.SeatBid {
 		for i := 0; i < len(sb.Bid); i++ {
 			bid := sb.Bid[i]
+			impVideo := &openrtb_ext.ExtBidPrebidVideo{}
+
+			if len(bid.Cat) > 1 {
+				bid.Cat = bid.Cat[0:1]
+			}
+
+			var bidExt *pubmaticBidExt
+			bidType := openrtb_ext.BidTypeBanner
+			if err := json.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt != nil {
+				if bidExt.VideoCreativeInfo != nil && bidExt.VideoCreativeInfo.Duration != nil {
+					impVideo.Duration = *bidExt.VideoCreativeInfo.Duration
+				}
+				bidType = getBidType(bidExt)
+			}
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
-				Bid:     &bid,
-				BidType: getBidType(bid.Ext),
+				Bid:      &bid,
+				BidType:  bidType,
+				BidVideo: impVideo,
 			})
 
 		}
@@ -560,28 +587,20 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 }
 
 // getBidType returns the bid type specified in the response bid.ext
-func getBidType(bidExt json.RawMessage) openrtb_ext.BidType {
+func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType {
 	// setting "banner" as the default bid type
 	bidType := openrtb_ext.BidTypeBanner
-	if bidExt != nil {
-		bidExtMap := make(map[string]interface{})
-		extbyte, err := json.Marshal(bidExt)
-		if err == nil {
-			err = json.Unmarshal(extbyte, &bidExtMap)
-			if err == nil && bidExtMap[bidTypeExtKey] != nil {
-				bidTypeVal := int(bidExtMap[bidTypeExtKey].(float64))
-				switch bidTypeVal {
-				case 0:
-					bidType = openrtb_ext.BidTypeBanner
-				case 1:
-					bidType = openrtb_ext.BidTypeVideo
-				case 2:
-					bidType = openrtb_ext.BidTypeNative
-				default:
-					// default value is banner
-					bidType = openrtb_ext.BidTypeBanner
-				}
-			}
+	if bidExt != nil && bidExt.BidType != nil {
+		switch *bidExt.BidType {
+		case 0:
+			bidType = openrtb_ext.BidTypeBanner
+		case 1:
+			bidType = openrtb_ext.BidTypeVideo
+		case 2:
+			bidType = openrtb_ext.BidTypeNative
+		default:
+			// default value is banner
+			bidType = openrtb_ext.BidTypeBanner
 		}
 	}
 	return bidType
diff --git a/config/config.go b/config/config.go
index a82dbb5e..8545523d 100755
--- a/config/config.go
+++ b/config/config.go
@@ -896,13 +896,14 @@ func SetupViper(v *viper.Viper, filename string) {
 	/*  Loopback:   127.0.0.0/8
 	/*
 	/* IPv6
-	/*  Loopback:     ::1/128
-	/*  Unique Local: fc00::/7
-	/*  Link Local:   fe80::/10
-	/*  Multicast:    ff00::/8
+	/*  Loopback:      ::1/128
+	/*  Documentation: 2001:db8::/32
+	/*  Unique Local:  fc00::/7
+	/*  Link Local:    fe80::/10
+	/*  Multicast:     ff00::/8
 	*/
 	v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"})
-	v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8"})
+	v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "2001:db8::/32"})
 
 	// Set environment variable support:
 	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md
index c366db3a..54a0a5b1 100644
--- a/docs/bidders/openx.md
+++ b/docs/bidders/openx.md
@@ -5,10 +5,13 @@ OpenX supports the following parameters:
 | property | type | required? | description | example |
 |----------|------|-----------|-------------|---------|
 | unit | string | required | The ad unit id | "10092842" |
-| delDomain | string | required | The delivery domain for the customer | "sademo-d.openx.net" |
+| delDomain | string | required\* | The delivery domain for the customer | "sademo-d.openx.net" |
+| platform | uuid | required\* | The platform id for the customer | "a3aece0c-9e80-4316-8deb-faf804779bd1" |
 | customFloor | number | optional | The minimum CPM price in USD | 1.50 - sets a $1.50 floor |
 | customParams | object | optional | User-defined targeting key-value pairs | {key1: "v1", key2: ["v2","v3"]} |
 
+\* At least one of `delDomain` or `platform` parameters is required.
+
 If you have any questions regarding setting up, please reach out to your account manager or 
 <support@openx.com>
 
@@ -59,4 +62,4 @@ If you have any questions regarding setting up, please reach out to your account
     },
   }
 }
-```
\ No newline at end of file
+```
diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md
index d0921618..b532923e 100644
--- a/docs/endpoints/openrtb2/auction.md
+++ b/docs/endpoints/openrtb2/auction.md
@@ -173,6 +173,7 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta
         },
         "includewinners": false, // Optional param defaulting to true
         "includebidderkeys": false // Optional param defaulting to true
+        "includeformat": false // Optional param defaulting to false
       }
     }
   }
@@ -184,6 +185,8 @@ For backwards compatibility the following strings will also be allowed as price
 
 One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether.
 
+The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above.
+
 MediaType PriceGranularity (PBS-Java only) - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. 
 
 ```
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 3f0258dd..5001e495 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -135,6 +135,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 				includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys,
 				includeCacheBids:  shouldCacheBids,
 				includeCacheVast:  shouldCacheVAST,
+				includeFormat:     requestExt.Prebid.Targeting.IncludeFormat,
 			}
 			targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData()
 		}
diff --git a/exchange/targeting.go b/exchange/targeting.go
index 994ad9e7..dca57653 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -22,6 +22,7 @@ type targetData struct {
 	includeBidderKeys bool
 	includeCacheBids  bool
 	includeCacheVast  bool
+	includeFormat     bool
 	// cacheHost and cachePath exist to supply cache host and path as targeting parameters
 	cacheHost string
 	cachePath string
@@ -53,6 +54,9 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 			if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok {
 				targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner)
 			}
+			if targData.includeFormat {
+				targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner)
+			}
 
 			if targData.cacheHost != "" {
 				targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner)
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index 3b297c7a..09ee375b 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -99,6 +99,9 @@ const (
 	HbSizeConstantKey   TargetingKey = "hb_size"
 	HbDealIDConstantKey TargetingKey = "hb_deal"
 
+	// HbFormatKey is the format of the bid. For example, "video", "banner"
+	HbFormatKey TargetingKey = "hb_format"
+
 	// HbCacheKey and HbVastCacheKey store UUIDs which can be used to fetch things from prebid cache.
 	// Callers should *never* assume that either of these exist, since the call to the cache may always fail.
 	//
diff --git a/openrtb_ext/imp_openx.go b/openrtb_ext/imp_openx.go
index e63595b0..2625cb38 100644
--- a/openrtb_ext/imp_openx.go
+++ b/openrtb_ext/imp_openx.go
@@ -3,6 +3,7 @@ package openrtb_ext
 // ExtImpOpenx defines the contract for bidrequest.imp[i].ext.openx
 type ExtImpOpenx struct {
 	Unit         string                 `json:"unit"`
+	Platform     string                 `json:"platform"`
 	DelDomain    string                 `json:"delDomain"`
 	CustomFloor  float64                `json:"customFloor"`
 	CustomParams map[string]interface{} `json:"customParams"`
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 86388f60..acfd4a1e 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -85,6 +85,7 @@ type ExtRequestTargeting struct {
 	IncludeWinners       bool                     `json:"includewinners"`
 	IncludeBidderKeys    bool                     `json:"includebidderkeys"`
 	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"`
+	IncludeFormat        bool                     `json:"includeformat"`
 	DurationRangeSec     []int                    `json:"durationrangesec"`
 }
 
diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml
index 0feca7cd..6ba563b4 100644
--- a/static/bidder-info/gumgum.yaml
+++ b/static/bidder-info/gumgum.yaml
@@ -4,3 +4,4 @@ capabilities:
   site:
     mediaTypes:
     - banner
+    - video
\ No newline at end of file
diff --git a/static/bidder-params/openx.json b/static/bidder-params/openx.json
index 93a672ed..6dbd1017 100644
--- a/static/bidder-params/openx.json
+++ b/static/bidder-params/openx.json
@@ -16,6 +16,11 @@
       "pattern": "\\.[a-zA-Z]{2,3}$",
       "format": "hostname"
     },
+    "platform": {
+      "type": "string",
+      "description": "The platform id for the customer.",
+      "format": "uuid"
+    },
     "customFloor": {
       "type": "number",
       "description": "The minimum CPM price in USD.",
@@ -26,6 +31,19 @@
       "description": "User-defined targeting key-value pairs."
     }
   },
-
-  "required": ["unit", "delDomain"]
+  "required": [
+    "unit"
+  ],
+  "anyOf": [
+    {
+      "required": [
+        "delDomain"
+      ]
+    },
+    {
+      "required": [
+        "platform"
+      ]
+    }
+  ]
 }


Test patch:
diff --git a/adapters/gumgum/gumgumtest/exemplary/video.json b/adapters/gumgum/gumgumtest/exemplary/video.json
new file mode 100644
index 00000000..ea76c733
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/exemplary/video.json
@@ -0,0 +1,106 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "minduration": 1,
+                    "maxduration": 2,
+                    "protocols": [
+                        1,
+                        2
+                    ],
+                    "w": 640,
+                    "h": 480,
+                    "startdelay": 1,
+                    "placement": 1,
+                    "linearity": 1
+                },
+                "ext": {
+                    "bidder": {
+                        "zone": "ggumtest"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://g2.gumgum.com/providers/prbds2s/bid",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "minduration": 1,
+                                "maxduration": 2,
+                                "protocols": [
+                                    1,
+                                    2
+                                ],
+                                "w": 640,
+                                "h": 480,
+                                "startdelay": 1,
+                                "placement": 1,
+                                "linearity": 1
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "zone": "ggumtest"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "15da721e-940a-4db6-8621-a1f93140b21b",
+                                    "impid": "video1",
+                                    "price": 15,
+                                    "adid": "59082",
+                                    "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"3.0\">\n    <Ad id=\"59082\">\n        <InLine>\n            <AdSystem>GumGum Video</AdSystem>\n            <AdTitle>\n                <![CDATA[\n                Pre-Roll Preview - Olay - Linear (:15)\n                ]]>\n            </AdTitle>\n            <Impression>\n                <![CDATA[ \n                http://or-g2.gumgum.com/ad/apvideo/impression?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                ]]>\n            </Impression>\n            <Creatives>\n                <Creative>\n                    <Linear>\n                        <Duration>00:00:15</Duration>\n                        <TrackingEvents>\n                            <Tracking event=\"start\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/play?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"firstQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed25?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"midpoint\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed50?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"thirdQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed75?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"complete\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed100?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                        </TrackingEvents>\n                        <VideoClicks>\n                            <ClickThrough>\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/click/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQICKHpXkDscfR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qWEwNqd4GFjWm0S6Hmy18v7I3SrGJPg7FzC1iGGOSMjfe1DJDplloE0qQcxKI9gI1aTJqG_G7PY6QP1Gz87Ojj7mLf40udAlRofZaq1FRdFXXgo0XIeg3J7o07-2TGPvBfdg0hIpis6TOWo-gcqbOKWOP24eiroudFCTDNH9QAi9dXKor0jUMLhKmvY6kvmNl8?du=\n                                ]]>\n                            </ClickThrough>\n                        </VideoClicks>\n                        <MediaFiles>\n                            <MediaFile bitrate=\"800\" delivery=\"progressive\" height=\"9\" type=\"video/mp4\" width=\"16\">\n                                <![CDATA[ https://c.gumgum.com/ads/com/procter_gamble/olay_daily_facials_q2_2017/pre_roll_demo/5_source_41397_100091.mp4\n                                ]]>\n                            </MediaFile>\n                        </MediaFiles>\n                    </Linear>\n                </Creative>\n            </Creatives>\n        <Impression><![CDATA[http://or-g2.gumgum.com/ad/view/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQJ8jwoXY9Y_XR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qXaMMkNa6YwX71oVLIAHL6p6DL-8M2FwYgSPFHW1P72slMYnDS0_PfnysQjnRJ2eQXGmpiIEg0seK48nGbq14RwpQtD8WOwrIySXeuk0TqdyUQE2RMlq2dV?c=15]]></Impression><Error><![CDATA[http://or-g2.gumgum.com/video/error?code=[ERRORCODE]&adid=59082&t=ggumtest]]></Error></InLine>\n    </Ad>\n</VAST>",
+                                    "cid": "3579",
+                                    "crid": "59082"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "15da721e-940a-4db6-8621-a1f93140b21b",
+                        "impid": "video1",
+                        "price": 15,
+                        "adid": "59082",
+                        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"3.0\">\n    <Ad id=\"59082\">\n        <InLine>\n            <AdSystem>GumGum Video</AdSystem>\n            <AdTitle>\n                <![CDATA[\n                Pre-Roll Preview - Olay - Linear (:15)\n                ]]>\n            </AdTitle>\n            <Impression>\n                <![CDATA[ \n                http://or-g2.gumgum.com/ad/apvideo/impression?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                ]]>\n            </Impression>\n            <Creatives>\n                <Creative>\n                    <Linear>\n                        <Duration>00:00:15</Duration>\n                        <TrackingEvents>\n                            <Tracking event=\"start\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/play?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"firstQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed25?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"midpoint\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed50?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"thirdQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed75?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"complete\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed100?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                        </TrackingEvents>\n                        <VideoClicks>\n                            <ClickThrough>\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/click/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQICKHpXkDscfR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qWEwNqd4GFjWm0S6Hmy18v7I3SrGJPg7FzC1iGGOSMjfe1DJDplloE0qQcxKI9gI1aTJqG_G7PY6QP1Gz87Ojj7mLf40udAlRofZaq1FRdFXXgo0XIeg3J7o07-2TGPvBfdg0hIpis6TOWo-gcqbOKWOP24eiroudFCTDNH9QAi9dXKor0jUMLhKmvY6kvmNl8?du=\n                                ]]>\n                            </ClickThrough>\n                        </VideoClicks>\n                        <MediaFiles>\n                            <MediaFile bitrate=\"800\" delivery=\"progressive\" height=\"9\" type=\"video/mp4\" width=\"16\">\n                                <![CDATA[ https://c.gumgum.com/ads/com/procter_gamble/olay_daily_facials_q2_2017/pre_roll_demo/5_source_41397_100091.mp4\n                                ]]>\n                            </MediaFile>\n                        </MediaFiles>\n                    </Linear>\n                </Creative>\n            </Creatives>\n        <Impression><![CDATA[http://or-g2.gumgum.com/ad/view/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQJ8jwoXY9Y_XR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qXaMMkNa6YwX71oVLIAHL6p6DL-8M2FwYgSPFHW1P72slMYnDS0_PfnysQjnRJ2eQXGmpiIEg0seK48nGbq14RwpQtD8WOwrIySXeuk0TqdyUQE2RMlq2dV?c=15]]></Impression><Error><![CDATA[http://or-g2.gumgum.com/video/error?code=[ERRORCODE]&adid=59082&t=ggumtest]]></Error></InLine>\n    </Ad>\n</VAST>",
+                        "cid": "3579",
+                        "crid": "59082"
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/gumgum/gumgumtest/params/race/video.json b/adapters/gumgum/gumgumtest/params/race/video.json
new file mode 100644
index 00000000..3ed28438
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+    "zone": "dc9d6be1"
+}
\ No newline at end of file
diff --git a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json
new file mode 100644
index 00000000..b2475cd7
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json
@@ -0,0 +1,36 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+        {
+            "id": "test-imp-id",
+            "video": {
+                "mimes": [
+                    "video/mp4"
+                ],
+                "protocols": [
+                    1,
+                    2
+                ],
+                "w": 640,
+                "h": 480,
+                "startdelay": 1,
+                "placement": 1,
+                "linearity": 1
+            },
+            "ext": {
+                "bidder": {
+                    "zone": "ggumtest"
+                }
+            }
+        }
+    ]
+    },
+    "expectedMakeRequestsErrors": [
+      {
+        "value": "Invalid or missing video field(s)",
+        "comparison": "literal"
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/openx/openxtest/exemplary/optional-params.json b/adapters/openx/openxtest/exemplary/optional-params.json
index 22555987..e8fcf394 100644
--- a/adapters/openx/openxtest/exemplary/optional-params.json
+++ b/adapters/openx/openxtest/exemplary/optional-params.json
@@ -11,6 +11,7 @@
           "bidder": {
             "unit": "539439964",
             "delDomain": "se-demo-d.openx.net",
+            "platform": "PLATFORM",
             "customFloor": 0.1,
             "customParams": {"foo": "bar"}
           }
@@ -40,7 +41,8 @@
           ],
           "ext": {
             "bc": "hb_pbs_1.0.0",
-            "delDomain": "se-demo-d.openx.net"
+            "delDomain": "se-demo-d.openx.net",
+            "platform": "PLATFORM"
           }
         }
       },
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index be086f5a..5dd236d9 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -674,18 +674,18 @@ func TestPubmaticSampleRequest(t *testing.T) {
 }
 
 func TestGetBidTypeVideo(t *testing.T) {
-	extJSON := `{"BidType":1}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 1
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeVideo {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeVideo, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeForMissingBidTypeExt(t *testing.T) {
-	extJSON := `{}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := pubmaticBidExt{}
+	actualBidTypeValue := getBidType(&pubmaticExt)
 	// banner is the default bid type when no bidType key is present in the bid.ext
 	if actualBidTypeValue != "banner" {
 		t.Errorf("Expected Bid Type value was: banner, actual value is: %v", actualBidTypeValue)
@@ -693,27 +693,30 @@ func TestGetBidTypeForMissingBidTypeExt(t *testing.T) {
 }
 
 func TestGetBidTypeBanner(t *testing.T) {
-	extJSON := `{"BidType":0}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 0
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeBanner {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeNative(t *testing.T) {
-	extJSON := `{"BidType":2}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 2
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeNative {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeNative, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeForUnsupportedCode(t *testing.T) {
-	extJSON := `{"BidType":99}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 99
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeBanner {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 25ed366e..4c874535 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -112,10 +112,14 @@
                   "h": 250,
                   "w": 300,
                   "dealid":"test deal",
+                  "cat" : ["IAB-1", "IAB-2"], 
                   "ext": {
                     "dspid": 6,
                     "deal_channel": 1,
-                    "BidType": 1
+                    "BidType": 1,
+                    "video" : {
+                      "duration" : 5  
+                    }
                   }
                 }]
               }
@@ -139,6 +143,9 @@
               "adid": "29681110",
               "adm": "some-test-ad",
               "adomain": ["pubmatic.com"],
+              "cat": [
+                "IAB-1"
+              ],
               "crid": "29681110",
               "w": 300,
               "h": 250,
@@ -146,12 +153,18 @@
               "ext": {
                 "dspid": 6,
                 "deal_channel": 1,
-                "BidType": 1
+                "BidType": 1,
+                "video" : {
+                  "duration" : 5  
+                }
               }
             },
-            "type": "video"
+            "type": "video",
+            "video" :{
+              "duration" : 5
+            }
           }
         ]
       }
     ]
-  }
\ No newline at end of file
+  }
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 16955e97..11b60db0 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -247,3 +247,212 @@ func (f *mockFetcher) GetId(bidder openrtb_ext.BidderName) (string, bool) {
 func mockServer(w http.ResponseWriter, req *http.Request) {
 	w.Write([]byte("{}"))
 }
+
+type TargetingTestData struct {
+	Description                string
+	TargetData                 targetData
+	Auction                    auction
+	IsApp                      bool
+	CategoryMapping            map[string]string
+	ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string
+}
+
+var bid123 *openrtb.Bid = &openrtb.Bid{
+	Price: 1.23,
+}
+
+var bid111 *openrtb.Bid = &openrtb.Bid{
+	Price:  1.11,
+	DealID: "mydeal",
+}
+var bid084 *openrtb.Bid = &openrtb.Bid{
+	Price: 0.84,
+}
+
+var TargetingTests []TargetingTestData = []TargetingTestData{
+	{
+		Description: "Targeting winners only (most basic targeting example)",
+		TargetData: targetData{
+			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			includeWinners:   true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder": "appnexus",
+					"hb_pb":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {},
+			},
+		},
+	},
+	{
+		Description: "Targeting on bidders only",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb_appnexus":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+				},
+			},
+		},
+	},
+	{
+		Description: "Full basic targeting with hd_format",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeWinners:    true,
+			includeBidderKeys: true,
+			includeFormat:     true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder":          "appnexus",
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb":              "1.20",
+					"hb_pb_appnexus":     "1.20",
+					"hb_format":          "banner",
+					"hb_format_appnexus": "banner",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+					"hb_format_rubicon": "banner",
+				},
+			},
+		},
+	},
+	{
+		Description: "Cache and deal targeting test",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+			cacheHost:         "cache.prebid.com",
+			cachePath:         "cache",
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid111,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+			cacheIds: map[*openrtb.Bid]string{
+				bid123: "55555",
+				bid111: "cacheme",
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus":   "appnexus",
+					"hb_pb_appnexus":       "1.20",
+					"hb_cache_id_appnexus": "55555",
+					"hb_cache_host_appnex": "cache.prebid.com",
+					"hb_cache_path_appnex": "cache",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon":    "rubicon",
+					"hb_pb_rubicon":        "1.10",
+					"hb_cache_id_rubicon":  "cacheme",
+					"hb_deal_rubicon":      "mydeal",
+					"hb_cache_host_rubico": "cache.prebid.com",
+					"hb_cache_path_rubico": "cache",
+				},
+			},
+		},
+	},
+}
+
+func TestSetTargeting(t *testing.T) {
+	for _, test := range TargetingTests {
+		auc := &test.Auction
+		// Set rounded prices from the auction data
+		auc.setRoundedPrices(test.TargetData.priceGranularity)
+		winningBids := make(map[string]*pbsOrtbBid)
+		// Set winning bids from the auction data
+		for imp, bidsByBidder := range auc.winningBidsByBidder {
+			for _, bid := range bidsByBidder {
+				if winningBid, ok := winningBids[imp]; ok {
+					if winningBid.bid.Price < bid.bid.Price {
+						winningBids[imp] = bid
+					}
+				} else {
+					winningBids[imp] = bid
+				}
+			}
+		}
+		auc.winningBids = winningBids
+		targData := test.TargetData
+		targData.setTargeting(auc, test.IsApp, test.CategoryMapping)
+		for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder {
+			for bidder, expected := range targetsByBidder {
+				assert.Equal(t,
+					expected,
+					auc.winningBidsByBidder[imp][bidder].bidTargets,
+					"Test: %s\nTargeting failed for bidder %s on imp %s.",
+					test.Description,
+					string(bidder),
+					imp)
+			}
+		}
+	}
+
+}