Repository: prebid/prebid-server
Pull request #1593 (2020-11-19 16:28:58)

Enforce GDPR privacy if there's an error parsing consent
I'm leaving this as a draft until #1591 is merged, at which point I'll rebase this and make it ready for review.

stale[bot]: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Patch:
diff --git a/adapters/adot/adot.go b/adapters/adot/adot.go
new file mode 100644
index 00000000..c58c4a1e
--- /dev/null
+++ b/adapters/adot/adot.go
@@ -0,0 +1,117 @@
+package adot
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"net/http"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+type adotBidExt struct {
+	Adot bidExt `json:"adot"`
+}
+
+type bidExt struct {
+	MediaType string `json:"media_type"`
+}
+
+// Builder builds a new instance of the Adot adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+// MakeRequests makes the HTTP requests which should be made to fetch bids.
+func (a *adapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var reqJSON []byte
+	var err error
+
+	if reqJSON, err = json.Marshal(request); err != nil {
+		return nil, []error{fmt.Errorf("unable to marshal openrtb request (%v)", err)}
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+
+	return []*adapters.RequestData{{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqJSON,
+		Headers: headers,
+	}}, nil
+}
+
+// MakeBids unpacks the server's response into Bids.
+// The bidder return a status code 204 when it cannot delivery an ad.
+func (a *adapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	bidsCapacity := 1
+	if len(bidResp.SeatBid) > 0 {
+		bidsCapacity = len(bidResp.SeatBid[0].Bid)
+	}
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(bidsCapacity)
+
+	for _, sb := range bidResp.SeatBid {
+		for i := range sb.Bid {
+			if bidType, err := getMediaTypeForBid(&sb.Bid[i]); err == nil {
+				bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+					Bid:     &sb.Bid[i],
+					BidType: bidType,
+				})
+			}
+		}
+	}
+
+	return bidResponse, nil
+}
+
+// getMediaTypeForBid determines which type of bid.
+func getMediaTypeForBid(bid *openrtb.Bid) (openrtb_ext.BidType, error) {
+	if bid == nil {
+		return "", fmt.Errorf("the bid request object is nil")
+	}
+
+	var impExt adotBidExt
+	if err := json.Unmarshal(bid.Ext, &impExt); err == nil {
+		switch impExt.Adot.MediaType {
+		case string(openrtb_ext.BidTypeBanner):
+			return openrtb_ext.BidTypeBanner, nil
+		case string(openrtb_ext.BidTypeVideo):
+			return openrtb_ext.BidTypeVideo, nil
+		case string(openrtb_ext.BidTypeNative):
+			return openrtb_ext.BidTypeNative, nil
+		}
+	}
+
+	return "", fmt.Errorf("unrecognized bid type in response from adot")
+}
diff --git a/amp/parse.go b/amp/parse.go
new file mode 100644
index 00000000..9e0e019f
--- /dev/null
+++ b/amp/parse.go
@@ -0,0 +1,110 @@
+package amp
+
+import (
+	"errors"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/mxmCherry/openrtb"
+)
+
+// Params defines the paramters of an AMP request.
+type Params struct {
+	Account         string
+	CanonicalURL    string
+	Consent         string
+	Debug           bool
+	Origin          string
+	Size            Size
+	Slot            string
+	StoredRequestID string
+	Timeout         *uint64
+}
+
+// Size defines size information of an AMP request.
+type Size struct {
+	Height         uint64
+	Multisize      []openrtb.Format
+	OverrideHeight uint64
+	OverrideWidth  uint64
+	Width          uint64
+}
+
+// ParseParams parses the AMP paramters from a HTTP request.
+func ParseParams(httpRequest *http.Request) (Params, error) {
+	query := httpRequest.URL.Query()
+
+	tagID := query.Get("tag_id")
+	if len(tagID) == 0 {
+		return Params{}, errors.New("AMP requests require an AMP tag_id")
+	}
+
+	params := Params{
+		Account:      query.Get("account"),
+		CanonicalURL: query.Get("curl"),
+		Consent:      chooseConsent(query.Get("consent_string"), query.Get("gdpr_consent")),
+		Debug:        query.Get("debug") == "1",
+		Origin:       query.Get("__amp_source_origin"),
+		Size: Size{
+			Height:         parseInt(query.Get("h")),
+			Multisize:      parseMultisize(query.Get("ms")),
+			OverrideHeight: parseInt(query.Get("oh")),
+			OverrideWidth:  parseInt(query.Get("ow")),
+			Width:          parseInt(query.Get("w")),
+		},
+		Slot:            query.Get("slot"),
+		StoredRequestID: tagID,
+		Timeout:         parseIntPtr(query.Get("timeout")),
+	}
+	return params, nil
+}
+
+func parseIntPtr(value string) *uint64 {
+	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+		return &parsed
+	}
+	return nil
+}
+
+func parseInt(value string) uint64 {
+	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+		return parsed
+	}
+	return 0
+}
+
+func parseMultisize(multisize string) []openrtb.Format {
+	if multisize == "" {
+		return nil
+	}
+
+	sizeStrings := strings.Split(multisize, ",")
+	sizes := make([]openrtb.Format, 0, len(sizeStrings))
+	for _, sizeString := range sizeStrings {
+		wh := strings.Split(sizeString, "x")
+		if len(wh) != 2 {
+			return nil
+		}
+		f := openrtb.Format{
+			W: parseInt(wh[0]),
+			H: parseInt(wh[1]),
+		}
+		if f.W == 0 && f.H == 0 {
+			return nil
+		}
+
+		sizes = append(sizes, f)
+	}
+	return sizes
+}
+
+func chooseConsent(consent, gdprConsent string) string {
+	if len(consent) > 0 {
+		return consent
+	}
+
+	// Fallback to 'gdpr_consent' for compatability until it's no longer used. This was our original
+	// implementation before the same AMP macro was reused for CCPA.
+	return gdprConsent
+}
diff --git a/config/config.go b/config/config.go
index f720d841..4d8f6ab0 100755
--- a/config/config.go
+++ b/config/config.go
@@ -799,6 +799,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.admixer.endpoint", "http://inv-nets.admixer.net/pbs.aspx")
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
 	v.SetDefault("adapters.adoppler.endpoint", "http://{{.AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{.AdUnit}}")
+	v.SetDefault("adapters.adot.endpoint", "https://dsp.adotmob.com/headerbidding/bidrequest")
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
 	v.SetDefault("adapters.adprime.endpoint", "http://delta.adprime.com/?c=o&m=ortb")
 	v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb")
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index afc32e5e..dc66163a 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"strconv"
 	"strings"
 	"time"
 
@@ -17,6 +16,7 @@ import (
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb"
 	accountService "github.com/prebid/prebid-server/account"
+	"github.com/prebid/prebid-server/amp"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -314,43 +314,41 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 	req = &openrtb.BidRequest{}
 	errs = nil
 
-	ampID := httpRequest.FormValue("tag_id")
-	if ampID == "" {
-		errs = []error{errors.New("AMP requests require an AMP tag_id")}
-		return
+	ampParams, err := amp.ParseParams(httpRequest)
+	if err != nil {
+		return nil, []error{err}
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond)
 	defer cancel()
 
-	storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampID}, nil)
+	storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil)
 	if len(errs) > 0 {
 		return nil, errs
 	}
 	if len(storedRequests) == 0 {
-		errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampID)}
+		errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampParams.StoredRequestID)}
 		return
 	}
 
 	// The fetched config becomes the entire OpenRTB request
-	requestJSON := storedRequests[ampID]
+	requestJSON := storedRequests[ampParams.StoredRequestID]
 	if err := json.Unmarshal(requestJSON, req); err != nil {
 		errs = []error{err}
 		return
 	}
 
-	debugParam := httpRequest.FormValue("debug")
-	if debugParam == "1" {
+	if ampParams.Debug {
 		req.Test = 1
 	}
 
 	// Two checks so users know which way the Imp check failed.
 	if len(req.Imp) == 0 {
-		errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampID)}
+		errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampParams.StoredRequestID)}
 		return
 	}
 	if len(req.Imp) > 1 {
-		errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampID, len(req.Imp))}
+		errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampParams.StoredRequestID, len(req.Imp))}
 		return
 	}
 
@@ -367,35 +365,30 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 		*req.Imp[0].Secure = 1
 	}
 
-	errs = deps.overrideWithParams(httpRequest, req)
+	errs = deps.overrideWithParams(ampParams, req)
 	return
 }
 
-func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *openrtb.BidRequest) []error {
+func (deps *endpointDeps) overrideWithParams(ampParams amp.Params, req *openrtb.BidRequest) []error {
 	if req.Site == nil {
 		req.Site = &openrtb.Site{}
 	}
 
 	// Override the stored request sizes with AMP ones, if they exist.
 	if req.Imp[0].Banner != nil {
-		width := parseFormInt(httpRequest, "w", 0)
-		height := parseFormInt(httpRequest, "h", 0)
-		overrideWidth := parseFormInt(httpRequest, "ow", 0)
-		overrideHeight := parseFormInt(httpRequest, "oh", 0)
-		if format := makeFormatReplacement(overrideWidth, overrideHeight, width, height, httpRequest.FormValue("ms")); len(format) != 0 {
+		if format := makeFormatReplacement(ampParams.Size); len(format) != 0 {
 			req.Imp[0].Banner.Format = format
-		} else if width != 0 {
-			setWidths(req.Imp[0].Banner.Format, width)
-		} else if height != 0 {
-			setHeights(req.Imp[0].Banner.Format, height)
+		} else if ampParams.Size.Width != 0 {
+			setWidths(req.Imp[0].Banner.Format, ampParams.Size.Width)
+		} else if ampParams.Size.Height != 0 {
+			setHeights(req.Imp[0].Banner.Format, ampParams.Size.Height)
 		}
 	}
 
-	canonicalURL := httpRequest.FormValue("curl")
-	if canonicalURL != "" {
-		req.Site.Page = canonicalURL
+	if ampParams.CanonicalURL != "" {
+		req.Site.Page = ampParams.CanonicalURL
 		// Fixes #683
-		if parsedURL, err := url.Parse(canonicalURL); err == nil {
+		if parsedURL, err := url.Parse(ampParams.CanonicalURL); err == nil {
 			domain := parsedURL.Host
 			if colonIndex := strings.LastIndex(domain, ":"); colonIndex != -1 {
 				domain = domain[:colonIndex]
@@ -406,14 +399,13 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 
 	setAmpExt(req.Site, "1")
 
-	setEffectiveAmpPubID(req, httpRequest.URL.Query())
+	setEffectiveAmpPubID(req, ampParams.Account)
 
-	slot := httpRequest.FormValue("slot")
-	if slot != "" {
-		req.Imp[0].TagID = slot
+	if ampParams.Slot != "" {
+		req.Imp[0].TagID = ampParams.Slot
 	}
 
-	policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL)
+	policyWriter, policyWriterErr := readPolicy(ampParams.Consent)
 	if policyWriterErr != nil {
 		return []error{policyWriterErr}
 	}
@@ -421,42 +413,38 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 		return []error{err}
 	}
 
-	if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil {
-		req.TMax = timeout - deps.cfg.AMPTimeoutAdjustment
+	if ampParams.Timeout != nil {
+		req.TMax = int64(*ampParams.Timeout) - deps.cfg.AMPTimeoutAdjustment
 	}
 
 	return nil
 }
 
-func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
+func makeFormatReplacement(size amp.Size) []openrtb.Format {
 	var formats []openrtb.Format
-	if overrideWidth != 0 && overrideHeight != 0 {
+	if size.OverrideWidth != 0 && size.OverrideHeight != 0 {
 		formats = []openrtb.Format{{
-			W: overrideWidth,
-			H: overrideHeight,
+			W: size.OverrideWidth,
+			H: size.OverrideHeight,
 		}}
-	} else if overrideWidth != 0 && height != 0 {
+	} else if size.OverrideWidth != 0 && size.Height != 0 {
 		formats = []openrtb.Format{{
-			W: overrideWidth,
-			H: height,
+			W: size.OverrideWidth,
+			H: size.Height,
 		}}
-	} else if width != 0 && overrideHeight != 0 {
+	} else if size.Width != 0 && size.OverrideHeight != 0 {
 		formats = []openrtb.Format{{
-			W: width,
-			H: overrideHeight,
+			W: size.Width,
+			H: size.OverrideHeight,
 		}}
-	} else if width != 0 && height != 0 {
+	} else if size.Width != 0 && size.Height != 0 {
 		formats = []openrtb.Format{{
-			W: width,
-			H: height,
+			W: size.Width,
+			H: size.Height,
 		}}
 	}
 
-	if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 {
-		formats = append(formats, parsedSizes...)
-	}
-
-	return formats
+	return append(formats, size.Multisize...)
 }
 
 func setWidths(formats []openrtb.Format, width uint64) {
@@ -471,42 +459,6 @@ func setHeights(formats []openrtb.Format, height uint64) {
 	}
 }
 
-func parseMultisize(multisize string) []openrtb.Format {
-	if multisize == "" {
-		return nil
-	}
-
-	sizeStrings := strings.Split(multisize, ",")
-	sizes := make([]openrtb.Format, 0, len(sizeStrings))
-	for _, sizeString := range sizeStrings {
-		wh := strings.Split(sizeString, "x")
-		if len(wh) != 2 {
-			return nil
-		}
-		f := openrtb.Format{
-			W: parseIntErrorless(wh[0], 0),
-			H: parseIntErrorless(wh[1], 0),
-		}
-		if f.W == 0 && f.H == 0 {
-			return nil
-		}
-
-		sizes = append(sizes, f)
-	}
-	return sizes
-}
-
-func parseFormInt(req *http.Request, value string, defaultTo uint64) uint64 {
-	return parseIntErrorless(req.FormValue(value), defaultTo)
-}
-
-func parseIntErrorless(value string, defaultTo uint64) uint64 {
-	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
-		return parsed
-	}
-	return defaultTo
-}
-
 // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined.
 // If the user didn't include them, default those here.
 func defaultRequestExt(req *openrtb.BidRequest) (errs []error) {
@@ -563,9 +515,7 @@ func setAmpExt(site *openrtb.Site, value string) {
 	}
 }
 
-func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) {
-	consent := readConsentFromURL(url)
-
+func readPolicy(consent string) (privacy.PolicyWriter, error) {
 	if len(consent) == 0 {
 		return privacy.NilPolicyWriter{}, nil
 	}
@@ -583,17 +533,8 @@ func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) {
 	}
 }
 
-func readConsentFromURL(url *url.URL) string {
-	if v := url.Query().Get("consent_string"); v != "" {
-		return v
-	}
-
-	// Fallback to 'gdpr_consent' for compatability until it's no longer used by AMP.
-	return url.Query().Get("gdpr_consent")
-}
-
 // Sets the effective publisher ID for amp request
-func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) {
+func setEffectiveAmpPubID(req *openrtb.BidRequest, account string) {
 	var pub *openrtb.Publisher
 	if req.App != nil {
 		if req.App.Publisher == nil {
@@ -608,10 +549,9 @@ func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) {
 	}
 
 	if pub.ID == "" {
-		// For amp requests, the publisher ID could be sent via the account
-		// query string
-		if acc := urlQueryParams.Get("account"); acc != "" && acc != "ACCOUNT_ID" {
-			pub.ID = acc
+		// ACCOUNT_ID is the unresolved macro name and should be ignored.
+		if account != "" && account != "ACCOUNT_ID" {
+			pub.ID = account
 		}
 	}
 }
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 9095ec1e..97f8adfe 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -13,6 +13,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/admixer"
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adoppler"
+	"github.com/prebid/prebid-server/adapters/adot"
 	"github.com/prebid/prebid-server/adapters/adpone"
 	"github.com/prebid/prebid-server/adapters/adprime"
 	"github.com/prebid/prebid-server/adapters/adtarget"
@@ -112,6 +113,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderAdOcean:          adocean.Builder,
 		openrtb_ext.BidderAdoppler:         adoppler.Builder,
 		openrtb_ext.BidderAdpone:           adpone.Builder,
+		openrtb_ext.BidderAdot:             adot.Builder,
 		openrtb_ext.BidderAdprime:          adprime.Builder,
 		openrtb_ext.BidderAdtarget:         adtarget.Builder,
 		openrtb_ext.BidderAdtelligent:      adtelligent.Builder,
diff --git a/exchange/utils.go b/exchange/utils.go
index 21c31a29..d574c2a6 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -118,11 +118,13 @@ func cleanOpenRTBRequests(ctx context.Context,
 		if gdprEnforced {
 			var publisherID = req.LegacyLabels.PubID
 			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent)
-			privacyEnforcement.GDPRGeo = !geo && err == nil
-			privacyEnforcement.GDPRID = !id && err == nil
-		} else {
-			privacyEnforcement.GDPRGeo = false
-			privacyEnforcement.GDPRID = false
+			if err == nil {
+				privacyEnforcement.GDPRGeo = !geo
+				privacyEnforcement.GDPRID = !id
+			} else {
+				privacyEnforcement.GDPRGeo = true
+				privacyEnforcement.GDPRID = true
+			}
 		}
 
 		privacyEnforcement.Apply(bidderRequest.BidRequest)
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index f7f83125..80a45108 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -51,6 +51,7 @@ const (
 	BidderAdmixer          BidderName = "admixer"
 	BidderAdOcean          BidderName = "adocean"
 	BidderAdoppler         BidderName = "adoppler"
+	BidderAdot             BidderName = "adot"
 	BidderAdpone           BidderName = "adpone"
 	BidderAdprime          BidderName = "adprime"
 	BidderAdtarget         BidderName = "adtarget"
@@ -148,6 +149,7 @@ func CoreBidderNames() []BidderName {
 		BidderAdmixer,
 		BidderAdOcean,
 		BidderAdoppler,
+		BidderAdot,
 		BidderAdpone,
 		BidderAdprime,
 		BidderAdtarget,
diff --git a/openrtb_ext/imp_adot.go b/openrtb_ext/imp_adot.go
new file mode 100644
index 00000000..5d741589
--- /dev/null
+++ b/openrtb_ext/imp_adot.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ExtImpAdot struct {
+	Parallax    bool   `json:"parallax,omitempty"`
+	PlacementId string `json:"placementId,omitempty"`
+}
diff --git a/static/bidder-info/adot.yaml b/static/bidder-info/adot.yaml
new file mode 100644
index 00000000..529ff3ae
--- /dev/null
+++ b/static/bidder-info/adot.yaml
@@ -0,0 +1,14 @@
+maintainer:
+  email: "admin@we-are-adot.com"
+modifyingVastXmlAllowed: false
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
diff --git a/static/bidder-params/adot.json b/static/bidder-params/adot.json
new file mode 100644
index 00000000..fa2b8533
--- /dev/null
+++ b/static/bidder-params/adot.json
@@ -0,0 +1,17 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "The Adot Adapter Params",
+    "description": "A schema which validates params accepted by Adot adapter",
+    "type": "object",
+    "properties": {
+        "placementId": {
+            "type": "string",
+            "description": "An ID which identifies this placement of the impression"
+        },
+        "parallax": {
+            "type": "boolean",
+            "description": "It determines if the wanted advertising format is a parallax."
+        }
+    },
+    "required": []
+}
\ No newline at end of file


Test patch:
diff --git a/adapters/adot/adot_test.go b/adapters/adot/adot_test.go
new file mode 100644
index 00000000..fca6b303
--- /dev/null
+++ b/adapters/adot/adot_test.go
@@ -0,0 +1,76 @@
+package adot
+
+import (
+	"encoding/json"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+const testsBidderEndpoint = "https://dsp.adotmob.com/headerbidding/bidrequest"
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{
+		Endpoint: testsBidderEndpoint})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "adottest", bidder)
+}
+
+//Test the media type error
+func TestMediaTypeError(t *testing.T) {
+	_, err := getMediaTypeForBid(nil)
+
+	assert.Error(t, err)
+
+	byteInvalid, _ := json.Marshal(&adotBidExt{Adot: bidExt{"invalid"}})
+	_, err = getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteInvalid)})
+
+	assert.Error(t, err)
+}
+
+//Test the bid response when the bidder return a status code 204
+func TestBidResponseNoContent(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{
+		Endpoint: "https://dsp.adotmob.com/headerbidding/bidrequest"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	bidResponse, err := bidder.MakeBids(nil, nil, &adapters.ResponseData{StatusCode: 204})
+	if bidResponse != nil {
+		t.Fatalf("the bid response should be nil since the bidder status is No Content")
+	} else if err != nil {
+		t.Fatalf("the error should be nil since the bidder status is 204 : No Content")
+	}
+}
+
+//Test the media type for a bid response
+func TestMediaTypeForBid(t *testing.T) {
+	byteBanner, _ := json.Marshal(&adotBidExt{Adot: bidExt{"banner"}})
+	byteVideo, _ := json.Marshal(&adotBidExt{Adot: bidExt{"video"}})
+	byteNative, _ := json.Marshal(&adotBidExt{Adot: bidExt{"native"}})
+
+	bidTypeBanner, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteBanner)})
+	if bidTypeBanner != openrtb_ext.BidTypeBanner {
+		t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeBanner, openrtb_ext.BidTypeBanner)
+	}
+
+	bidTypeVideo, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteVideo)})
+	if bidTypeVideo != openrtb_ext.BidTypeVideo {
+		t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeVideo, openrtb_ext.BidTypeVideo)
+	}
+
+	bidTypeNative, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteNative)})
+	if bidTypeNative != openrtb_ext.BidTypeNative {
+		t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeNative, openrtb_ext.BidTypeVideo)
+	}
+}
diff --git a/adapters/adot/adottest/exemplary/simple-banner.json b/adapters/adot/adottest/exemplary/simple-banner.json
new file mode 100644
index 00000000..1b8cb986
--- /dev/null
+++ b/adapters/adot/adottest/exemplary/simple-banner.json
@@ -0,0 +1,94 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-banner-id",
+    "imp": [
+      {
+        "id": "test-imp-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+      "body": {
+        "id": "test-request-banner-id",
+        "imp": [
+          {
+            "id": "test-imp-banner-id",
+            "banner": {
+              "format": [
+                {
+                  "w": 320,
+                  "h": 250
+                }
+              ],
+              "w": 320,
+              "h": 250
+            },
+            "ext": {
+              "adot": {}
+            }
+          }
+        ]
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-banner-id",
+        "seatbid": [{
+          "seat": "adot",
+          "bid": [{
+            "id": "test-request-banner-id",
+            "impid": "test-imp-banner-id",
+            "price": 1.16346,
+            "adm": "some-test-ad",
+            "w": 320,
+            "h": 50,
+            "ext": {
+              "adot": {
+                "media_type": "banner"
+              }
+            }
+          }]
+        }
+        ],
+        "cur": "USD"
+      }
+    }
+  }],
+
+  "expectedBidResponses": [{
+      "currency": "USD",
+      "bids": [{
+          "bid": {
+            "id": "test-request-banner-id",
+            "impid": "test-imp-banner-id",
+            "price": 1.16346,
+            "adm": "some-test-ad",
+            "w": 320,
+            "h": 50,
+            "ext": {
+              "adot": {
+                "media_type": "banner"
+              }
+            }
+          },
+        "type": "banner"
+        }]
+    }]
+}
+
diff --git a/adapters/adot/adottest/exemplary/simple-interstitial.json b/adapters/adot/adottest/exemplary/simple-interstitial.json
new file mode 100644
index 00000000..0e9b573a
--- /dev/null
+++ b/adapters/adot/adottest/exemplary/simple-interstitial.json
@@ -0,0 +1,101 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-inter-id",
+    "imp": [
+      {
+        "id": "test-imp-inter-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 480
+            }
+          ],
+          "w": 320,
+          "h": 480
+        },
+        "instl":1,
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-inter-id",
+          "imp": [
+            {
+              "id": "test-imp-inter-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 480
+                  }
+                ],
+                "w": 320,
+                "h": 480
+              },
+              "instl":1,
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [{
+                "id": "test-request-inter-id",
+                "impid": "test-imp-inter-id",
+                "adm": "some-test-ad",
+                "price": 1.16346,
+                "w": 320,
+                "h": 480,
+                "ext": {
+                  "adot": {
+                    "media_type": "banner"
+                  }
+                }
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+        "bids": [{
+            "bid": {
+                "id": "test-request-inter-id",
+                "impid": "test-imp-inter-id",
+                "price":  1.16346,
+                "adm": "some-test-ad",
+                "w": 320,
+                "h": 480,
+                "ext": {
+                    "adot": {
+                        "media_type": "banner"
+                    }
+                }
+            },
+            "type": "banner"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/adot/adottest/exemplary/simple-native.json b/adapters/adot/adottest/exemplary/simple-native.json
new file mode 100644
index 00000000..5fa3c70f
--- /dev/null
+++ b/adapters/adot/adottest/exemplary/simple-native.json
@@ -0,0 +1,88 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-native-id",
+    "imp": [
+      {
+        "id": "test-imp-native-id",
+        "native": {
+          "request": "test-native",
+          "ver": "1.1"
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-native-id",
+          "imp": [
+            {
+              "id": "test-imp-native-id",
+              "native": {
+                "request": "test-native",
+                "ver": "1.1"
+              },
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [{
+                "id": "test-request-native-id",
+                "impid": "test-imp-native-id",
+                "price": 1.16346,
+                "adm" : "native-ad",
+                "w": 300,
+                "h": 250,
+                "ext": {
+                  "adot": {
+                    "media_type": "native"
+                  }
+                }
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-native-id",
+            "impid": "test-imp-native-id",
+            "price": 1.16346,
+            "adm" : "native-ad",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "adot": {
+                "media_type": "native"
+              }
+            }
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/adot/adottest/exemplary/simple-video.json b/adapters/adot/adottest/exemplary/simple-video.json
new file mode 100644
index 00000000..a453c4b9
--- /dev/null
+++ b/adapters/adot/adottest/exemplary/simple-video.json
@@ -0,0 +1,120 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-video-id",
+    "imp": [
+      {
+        "id": "test-imp-video-id",
+        "video": {
+          "w": 300,
+          "h": 250,
+          "maxduration": 60,
+          "minduration": 1,
+          "api": [
+            1,
+            2,
+            5,
+            6,
+            7
+          ],
+          "mimes": [
+            "video\/mp4"
+          ],
+          "placement": 4,
+          "protocols": [
+            2
+          ]
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-video-id",
+          "imp": [
+            {
+              "id": "test-imp-video-id",
+              "video": {
+                "w": 300,
+                "h": 250,
+                "maxduration": 60,
+                "minduration": 1,
+                "api": [
+                  1,
+                  2,
+                  5,
+                  6,
+                  7
+                ],
+                "mimes": [
+                  "video\/mp4"
+                ],
+                "placement": 4,
+                "protocols": [
+                  2
+                ]
+              },
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [{
+                "id": "test-request-video-id",
+                "impid": "test-imp-video-id",
+                "price": 1.16346,
+                "adm": "some-test-ad",
+                "w": 300,
+                "h": 250,
+                "ext": {
+                  "adot": {
+                    "media_type": "video"
+                  }
+                }
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-video-id",
+            "impid": "test-imp-video-id",
+            "price": 1.16346,
+            "adm": "some-test-ad",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "adot": {
+                "media_type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/adot/adottest/params/race/banner.json b/adapters/adot/adottest/params/race/banner.json
new file mode 100644
index 00000000..ada77aa4
--- /dev/null
+++ b/adapters/adot/adottest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+  "placementId": "ee234aac-113"
+}
\ No newline at end of file
diff --git a/adapters/adot/adottest/params/race/video.json b/adapters/adot/adottest/params/race/video.json
new file mode 100644
index 00000000..37808cd2
--- /dev/null
+++ b/adapters/adot/adottest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+  "placementId": "ee234aac-112"
+}
\ No newline at end of file
diff --git a/adapters/adot/adottest/supplemental/simple-audio.json b/adapters/adot/adottest/supplemental/simple-audio.json
new file mode 100644
index 00000000..9be53c4c
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/simple-audio.json
@@ -0,0 +1,66 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-audio-request",
+    "imp": [
+      {
+        "id": "unsupported-audio-imp",
+        "audio": {
+          "mimes": ["video/mp4"]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 1
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "unsupported-audio-request",
+          "imp": [
+            {
+              "id": "unsupported-audio-imp",
+              "audio": {
+                "mimes": ["video/mp4"]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [{
+                "id": "test-request-audio-id",
+                "impid": "test-imp-audio-id",
+                "price": 1.16346,
+                "adm": "some-audio-ad",
+                "ext": {
+                  "adot": {
+                    "media_type": "audio"
+                  }
+                }
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": []
+}
diff --git a/adapters/adot/adottest/supplemental/simple-parallax.json b/adapters/adot/adottest/supplemental/simple-parallax.json
new file mode 100644
index 00000000..4ee2ebc2
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/simple-parallax.json
@@ -0,0 +1,103 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-parallax-id",
+    "imp": [
+      {
+        "id": "test-imp-parallax-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ],
+          "w": 300,
+          "h": 600
+        },
+        "ext": {
+          "adot": {
+            "parallax": true
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-parallax-id",
+          "imp": [
+            {
+              "id": "test-imp-parallax-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ],
+                "w": 300,
+                "h": 600
+              },
+              "ext": {
+                "adot": {
+                  "parallax": true
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [{
+                "id": "test-request-parallax-id",
+                "impid": "test-imp-parallax-id",
+                "price": 0.5,
+                "adm": "some-test-ad",
+                "w": 300,
+                "h": 600,
+                "ext": {
+                  "adot": {
+                    "media_type": "banner"
+                  }
+                }
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-parallax-id",
+            "impid": "test-imp-parallax-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "adot": {
+                "media_type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/adot/adottest/supplemental/status_204.json b/adapters/adot/adottest/supplemental/status_204.json
new file mode 100644
index 00000000..44a895b5
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/status_204.json
@@ -0,0 +1,39 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-banner-id",
+    "imp": [
+      {
+        "id": "test-imp-banner-id",
+        "banner": {},
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-banner-id",
+          "imp": [
+            {
+              "id": "test-imp-banner-id",
+              "banner": {},
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
+
diff --git a/adapters/adot/adottest/supplemental/status_400.json b/adapters/adot/adottest/supplemental/status_400.json
new file mode 100644
index 00000000..22328fd9
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/status_400.json
@@ -0,0 +1,62 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-banner-id",
+    "imp": [
+      {
+        "id": "test-imp-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-banner-id",
+          "imp": [
+            {
+              "id": "test-imp-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ],
+                "w": 320,
+                "h": 250
+              },
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 400,
+        "body": {}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
+
diff --git a/adapters/adot/adottest/supplemental/status_500.json b/adapters/adot/adottest/supplemental/status_500.json
new file mode 100644
index 00000000..879bb8c5
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/status_500.json
@@ -0,0 +1,62 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-banner-id",
+    "imp": [
+      {
+        "id": "test-imp-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-banner-id",
+          "imp": [
+            {
+              "id": "test-imp-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ],
+                "w": 320,
+                "h": 250
+              },
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 500,
+        "body": {}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 500. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
+
diff --git a/adapters/adot/adottest/supplemental/unmarshal_error.json b/adapters/adot/adottest/supplemental/unmarshal_error.json
new file mode 100644
index 00000000..a87e1189
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/unmarshal_error.json
@@ -0,0 +1,62 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-banner-id",
+    "imp": [
+      {
+        "id": "test-imp-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-banner-id",
+          "imp": [
+            {
+              "id": "test-imp-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ],
+                "w": 320,
+                "h": 250
+              },
+              "ext": {
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": "fail for unmarshal"
+        }
+    }],
+
+  "expectedMakeBidsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse",
+      "comparison": "literal"
+    }
+  ]
+}
+
diff --git a/adapters/adot/params_test.go b/adapters/adot/params_test.go
new file mode 100644
index 00000000..2f7b4b9a
--- /dev/null
+++ b/adapters/adot/params_test.go
@@ -0,0 +1,53 @@
+package adot
+
+import (
+	"encoding/json"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"testing"
+)
+
+// This file actually intends to test static/bidder-params/adot.json
+//
+// These also validate the format of the external API: request.imp[i].ext.adot
+
+// TestValidParams makes sure that the adot schema accepts all imp.ext fields which we intend to support.
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAdot, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected adot params: %s", validParam)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAdot, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{}`,
+	`{"placementId": "test-114"}`,
+	`{"placementId": "test-113", "parallax": true}`,
+	`{"placementId": "test-113", "parallax": false}`,
+}
+
+var invalidParams = []string{
+	`{"parallax": 1}`,
+	`{"placementId": 135123}`,
+	`{"placementId": 113, "parallax": 1}`,
+	`{"placementId": 142, "parallax": true}`,
+	`{"placementId": "test-114", "parallax": 1}`,
+}
diff --git a/amp/parse_test.go b/amp/parse_test.go
new file mode 100644
index 00000000..e2c82d71
--- /dev/null
+++ b/amp/parse_test.go
@@ -0,0 +1,168 @@
+package amp
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseParams(t *testing.T) {
+	var expectedTimeout uint64 = 42
+
+	testCases := []struct {
+		description    string
+		query          string
+		expectedParams Params
+		expectedError  string
+	}{
+		{
+			description:   "Empty",
+			query:         "",
+			expectedError: "AMP requests require an AMP tag_id",
+		},
+		{
+			description: "All Fields",
+			query: "tag_id=anyTagID&account=anyAccount&curl=anyCurl&consent_string=anyConsent&debug=1&__amp_source_origin=anyOrigin" +
+				"&slot=anySlot&timeout=42&h=1&w=2&oh=3&ow=4&ms=10x11,12x13",
+			expectedParams: Params{
+				Account:         "anyAccount",
+				CanonicalURL:    "anyCurl",
+				Consent:         "anyConsent",
+				Debug:           true,
+				Origin:          "anyOrigin",
+				Slot:            "anySlot",
+				StoredRequestID: "anyTagID",
+				Timeout:         &expectedTimeout,
+				Size: Size{
+					Height:         1,
+					OverrideHeight: 3,
+					OverrideWidth:  4,
+					Width:          2,
+					Multisize: []openrtb.Format{
+						{W: 10, H: 11}, {W: 12, H: 13},
+					},
+				},
+			},
+		},
+		{
+			description:    "Integer Values Ignored If Invalid",
+			query:          "tag_id=anyTagID&h=invalid&w=invalid&oh=invalid&ow=invalid&ms=invalid",
+			expectedParams: Params{StoredRequestID: "anyTagID"},
+		},
+		{
+			description:    "consent_string Preferred Over gdpr_consent",
+			query:          "tag_id=anyTagID&consent_string=consent1&gdpr_consent=consent2",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"},
+		},
+		{
+			description:    "consent_string Preferred Over gdpr_consent - Order Doesn't Matter",
+			query:          "tag_id=anyTagID&gdpr_consent=consent2&consent_string=consent1",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"},
+		},
+		{
+			description:    "Just gdpr_consent",
+			query:          "tag_id=anyTagID&gdpr_consent=consent2",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent2"},
+		},
+		{
+			description:    "Debug 0",
+			query:          "tag_id=anyTagID&debug=0",
+			expectedParams: Params{StoredRequestID: "anyTagID", Debug: false},
+		},
+		{
+			description:    "Debug Ignored If Invalid",
+			query:          "tag_id=anyTagID&debug=invalid",
+			expectedParams: Params{StoredRequestID: "anyTagID", Debug: false},
+		},
+	}
+
+	for _, test := range testCases {
+		httpRequest, err := http.NewRequest("GET", "http://any.url/anypage?"+test.query, nil)
+		assert.NoError(t, err, test.description+":request")
+
+		params, err := ParseParams(httpRequest)
+		assert.Equal(t, test.expectedParams, params, test.description+":params")
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description+":err")
+		} else {
+			assert.EqualError(t, err, test.expectedError)
+		}
+	}
+}
+
+func TestParseMultisize(t *testing.T) {
+	testCases := []struct {
+		description     string
+		multisize       string
+		expectedFormats []openrtb.Format
+	}{
+		{
+			description:     "Empty",
+			multisize:       "",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One",
+			multisize:       "1x2",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}},
+		},
+		{
+			description:     "Many",
+			multisize:       "1x2,3x4",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 3, H: 4}},
+		},
+		{
+			// Existing Behavior: The " 3" token in the second size is parsed as 0.
+			description:     "Many With Space - Quirky Result",
+			multisize:       "1x2, 3x4",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 0, H: 4}},
+		},
+		{
+			description:     "One - Zero Size - Ignored",
+			multisize:       "0x0",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Zero Size - All Ignored",
+			multisize:       "0x0,3x4",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - Extra Dimension - Ignored",
+			multisize:       "1x2x3",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Extra Dimension - All Ignored",
+			multisize:       "1x2x3,4x5",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - Invalid Values - Ignored",
+			multisize:       "INVALIDxINVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Invalid Values - All Ignored",
+			multisize:       "1x2,INVALIDxINVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - No Pair - Ignored",
+			multisize:       "INVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - No Pair - All Ignored",
+			multisize:       "1x2,INVALID",
+			expectedFormats: nil,
+		},
+	}
+
+	for _, test := range testCases {
+		result := parseMultisize(test.multisize)
+		assert.ElementsMatch(t, test.expectedFormats, result, test.description)
+	}
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 746d436d..97e99c6b 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -1031,14 +1031,12 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool,
 
 func TestSetEffectiveAmpPubID(t *testing.T) {
 	testPubID := "test-pub"
-	testURLQueryParams := url.Values{}
-	testURLQueryParams.Add("account", testPubID)
 
 	testCases := []struct {
-		description    string
-		req            *openrtb.BidRequest
-		urlQueryParams url.Values
-		expectedPubID  string
+		description   string
+		req           *openrtb.BidRequest
+		account       string
+		expectedPubID string
 	}{
 		{
 			description: "No publisher ID provided",
@@ -1072,7 +1070,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 			expectedPubID: testPubID,
 		},
 		{
-			description: "Publisher ID present in account query parameter",
+			description: "Publisher ID present in account parameter",
 			req: &openrtb.BidRequest{
 				App: &openrtb.App{
 					Publisher: &openrtb.Publisher{
@@ -1080,8 +1078,8 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 					},
 				},
 			},
-			urlQueryParams: testURLQueryParams,
-			expectedPubID:  testPubID,
+			account:       testPubID,
+			expectedPubID: testPubID,
 		},
 		{
 			description: "req.Site.Publisher present but ID set to empty string",
@@ -1097,7 +1095,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		setEffectiveAmpPubID(test.req, test.urlQueryParams)
+		setEffectiveAmpPubID(test.req, test.account)
 		if test.req.Site != nil {
 			assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID,
 				"should return the expected Publisher ID for test case: %s", test.description)
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index f9a04f25..e13f956b 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -20,7 +20,8 @@ import (
 //
 // It only allows appnexus for GDPR consent
 type permissionsMock struct {
-	personalInfoAllowed bool
+	personalInfoAllowed      bool
+	personalInfoAllowedError error
 }
 
 func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) {
@@ -32,7 +33,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 }
 
 func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string) (bool, bool, bool, error) {
-	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil
+	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError
 }
 
 func assertReq(t *testing.T, bidderRequests []BidderRequest,
@@ -1045,6 +1046,8 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 }
 
 func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
+	tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
+	tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA"
 	trueValue, falseValue := true, false
 
 	testCases := []struct {
@@ -1054,6 +1057,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 		gdpr                string
 		gdprConsent         string
 		gdprScrub           bool
+		permissionsError    error
 		userSyncIfAmbiguous bool
 		expectPrivacyLabels metrics.PrivacyLabels
 	}{
@@ -1074,7 +1078,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1086,7 +1090,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
+			gdprConsent:        tcf2Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1098,7 +1102,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "0",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1110,7 +1114,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    false,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1122,7 +1126,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &falseValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1134,7 +1138,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: nil,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1146,7 +1150,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: nil,
 			gdprHostEnabled:    false,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1158,7 +1162,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled:  nil,
 			gdprHostEnabled:     true,
 			gdpr:                "null",
-			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:         tcf1Consent,
 			gdprScrub:           true,
 			userSyncIfAmbiguous: false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
@@ -1171,7 +1175,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled:  nil,
 			gdprHostEnabled:     true,
 			gdpr:                "null",
-			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:         tcf1Consent,
 			gdprScrub:           false,
 			userSyncIfAmbiguous: true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
@@ -1179,6 +1183,19 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 				GDPRTCFVersion: "",
 			},
 		},
+		{
+			description:        "Enforce - error while checking if personal info is allowed",
+			gdprAccountEnabled: nil,
+			gdprHostEnabled:    true,
+			gdpr:               "1",
+			gdprConsent:        tcf1Consent,
+			gdprScrub:          true,
+			permissionsError:   errors.New("Some error"),
+			expectPrivacyLabels: metrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: metrics.TCFVersionV1,
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -1214,7 +1231,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			context.Background(),
 			auctionReq,
 			nil,
-			&permissionsMock{personalInfoAllowed: !test.gdprScrub},
+			&permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError},
 			test.userSyncIfAmbiguous,
 			privacyConfig)
 		result := results[0]
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 60ab2478..ec8920ee 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -97,6 +97,7 @@ func TestNewSyncerMap(t *testing.T) {
 		openrtb_ext.BidderAdgeneration: true,
 		openrtb_ext.BidderAdhese:       true,
 		openrtb_ext.BidderAdoppler:     true,
+		openrtb_ext.BidderAdot:         true,
 		openrtb_ext.BidderApplogy:      true,
 		openrtb_ext.BidderInMobi:       true,
 		openrtb_ext.BidderKidoz:        true,