diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ebf6f614df3..f9bb82598ff 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "dockerfile": "Dockerfile", "args": { // Update the VARIANT arg to pick a version of Go - "VARIANT": "1.20", + "VARIANT": "1.22", // Options "INSTALL_NODE": "false", "NODE_VERSION": "lts/*" diff --git a/Dockerfile b/Dockerfile index e118e19562e..3fad605c87c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,6 @@ FROM golang:1.21-alpine RUN apk add --update tini RUN mkdir -p /app/prebid-server/ WORKDIR /app/prebid-server/ - COPY ./ ./ RUN go mod download diff --git a/README.md b/README.md index 5db45115dec..0b1a02cd2bc 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ or compile a standalone binary using the command: ``` bash go build . ``` +**Note:** if building from source there are a couple dependencies to be aware of: +1. *Compile-time*. Some modules ship native code that requires `cgo` (comes with the `go` compiler) being enabled - by default it is and environment variable `CGO_ENABLED=1` do NOT set it to `0`. +2. *Compile-time*. `cgo` depends on the C-compiler, which usually is `gcc`, but can be changed by setting the value of `CC` env var, f.e. `CC=clang`. On ubuntu `gcc` can be installed via `sudo apt-get install gcc`. +3. *Runtime*. Some modules require `libatomic`. On ubuntu it is installed by running `sudo apt-get install libatomic1`. `libatomic1` is a dependency of `gcc`, so if you are building with `gcc` and running on the same machine, it is likely that `libatomic1` is already installed. + Ensure that you deploy the `/static` directory, as Prebid Server requires those files at startup. ## Developing diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index 5b6c56d2cee..b7358df6a5d 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -223,9 +223,10 @@ type expectedBidResponse struct { } type expectedBid struct { - Bid json.RawMessage `json:"bid"` - Type string `json:"type"` - Seat string `json:"seat"` + Bid json.RawMessage `json:"bid"` + Type string `json:"type"` + Seat string `json:"seat"` + Video json.RawMessage `json:"video,omitempty"` } // --------------------------------------- @@ -330,6 +331,9 @@ func diffBids(t *testing.T, description string, actual *adapters.TypedBid, expec assert.Equal(t, string(expected.Seat), string(actual.Seat), fmt.Sprintf(`%s.seat "%s" does not match expected "%s."`, description, string(actual.Seat), string(expected.Seat))) assert.Equal(t, string(expected.Type), string(actual.BidType), fmt.Sprintf(`%s.type "%s" does not match expected "%s."`, description, string(actual.BidType), string(expected.Type))) assert.NoError(t, diffOrtbBids(fmt.Sprintf("%s.bid", description), actual.Bid, expected.Bid)) + if expected.Video != nil { + assert.NoError(t, diffBidVideo(fmt.Sprintf("%s.video", description), actual.BidVideo, expected.Video)) + } } // diffOrtbBids compares the actual Bid made by the adapter to the expectation from the JSON file. @@ -346,6 +350,15 @@ func diffOrtbBids(description string, actual *openrtb2.Bid, expected json.RawMes return diffJson(description, actualJson, expected) } +func diffBidVideo(description string, actual *openrtb_ext.ExtBidPrebidVideo, expected json.RawMessage) error { + actualJson, err := json.Marshal(actual) + if err != nil { + return fmt.Errorf("%s failed to marshal actual Bid Video into JSON. %v", description, err) + } + + return diffJson(description, actualJson, []byte(expected)) +} + // diffJson compares two JSON byte arrays for structural equality. It will produce an error if either // byte array is not actually JSON. func diffJson(description string, actual []byte, expected []byte) error { diff --git a/adapters/adnuntius/adnuntius.go b/adapters/adnuntius/adnuntius.go index 4b823455815..ff9c4e57aad 100644 --- a/adapters/adnuntius/adnuntius.go +++ b/adapters/adnuntius/adnuntius.go @@ -34,6 +34,11 @@ type extDeviceAdnuntius struct { NoCookies bool `json:"noCookies,omitempty"` } +type adnAdvertiser struct { + LegalName string `json:"legalName,omitempty"` + Name string `json:"name,omitempty"` +} + type Ad struct { Bid struct { Amount float64 @@ -53,6 +58,7 @@ type Ad struct { LineItemId string Html string DestinationUrls map[string]string + Advertiser adnAdvertiser `json:"advertiser,omitempty"` } type AdUnit struct { @@ -159,7 +165,7 @@ func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter, noCookies bool } q.Set("tzo", fmt.Sprint(tzo)) - q.Set("format", "json") + q.Set("format", "prebid") url := endpointUrl + "?" + q.Encode() return url, nil @@ -335,6 +341,40 @@ func getGDPR(request *openrtb2.BidRequest) (string, string, error) { return gdpr, consent, nil } +func generateReturnExt(ad Ad, request *openrtb2.BidRequest) (json.RawMessage, error) { + // We always force the publisher to render + var adRender int8 = 0 + + var requestRegsExt *openrtb_ext.ExtRegs + if request.Regs != nil && request.Regs.Ext != nil { + if err := json.Unmarshal(request.Regs.Ext, &requestRegsExt); err != nil { + + return nil, fmt.Errorf("Failed to parse Ext information in Adnuntius: %v", err) + } + } + + if ad.Advertiser.Name != "" && requestRegsExt != nil && requestRegsExt.DSA != nil { + legalName := ad.Advertiser.Name + if ad.Advertiser.LegalName != "" { + legalName = ad.Advertiser.LegalName + } + ext := &openrtb_ext.ExtBid{ + DSA: &openrtb_ext.ExtBidDSA{ + AdRender: &adRender, + Paid: legalName, + Behalf: legalName, + }, + } + returnExt, err := json.Marshal(ext) + if err != nil { + return nil, fmt.Errorf("Failed to parse Ext information in Adnuntius: %v", err) + } + + return returnExt, nil + } + return nil, nil +} + func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2.BidRequest) (*openrtb2.Bid, []error) { creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64) @@ -376,6 +416,13 @@ func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2. } } + extJson, err := generateReturnExt(ad, request) + if err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Error extracting Ext: %s", err.Error()), + }} + } + adDomain := []string{} for _, url := range ad.DestinationUrls { domainArray := strings.Split(url, "/") @@ -395,6 +442,7 @@ func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2. Price: price * 1000, AdM: html, ADomain: adDomain, + Ext: extJson, } return &bid, nil diff --git a/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json b/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json index 3a50789e4dd..2472bce3c1c 100644 --- a/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json +++ b/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json index 2565fee93c9..caaf8892388 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json new file mode 100644 index 00000000000..0b44aa16dca --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv", + "advertiser": { + "name": "Name" + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240, + "ext": { + "dsa": { + "paid": "Name", + "behalf": "Name", + "adrender": 0 + } + } + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json new file mode 100644 index 00000000000..7999bd476aa --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv", + "advertiser": { + "name": "Name", + "legalName": "LegalName" + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240, + "ext": { + "dsa": { + "paid": "LegalName", + "behalf": "LegalName", + "adrender": 0 + } + } + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json new file mode 100644 index 00000000000..11cea9bcf66 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json b/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json index c73d69bad83..3e72810422c 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json @@ -38,7 +38,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://gdpr.url?consentString=CONSENT_STRING&format=json&gdpr=1&tzo=0", + "uri": "http://gdpr.url?consentString=CONSENT_STRING&format=prebid&gdpr=1&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json b/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json index d6301fe71cf..2078258cab7 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json b/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json index ebb25b2b7ad..35c9bca909c 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json index b0d74565771..dfec207bb24 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json index f1ddd3f7d5a..b8598b3aa90 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json @@ -35,7 +35,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json index d6f292d8cd5..7eb3d8afdb2 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json @@ -47,11 +47,11 @@ } ] }, - + "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { @@ -158,6 +158,6 @@ ], "currency": "NOK" } - + ] } diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json b/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json index 4b8e6de346e..99d23f2d3fc 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json index f3aebd99621..904345297ac 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json index 06593630c43..5c88e055789 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json @@ -32,7 +32,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json index 1987fb9d08e..48670ee1e03 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json new file mode 100644 index 00000000000..bf0365f60a9 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json @@ -0,0 +1,46 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": "" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "failed to parse URL: [failed to parse Adnuntius endpoint: failed to parse ExtRegs in Adnuntius GDPR check: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs]", + "comparison": "literal" + } + ] +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json index 1d4c5bf0747..691de79c25f 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json @@ -28,11 +28,11 @@ } ] }, - + "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json b/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json index bcfecfa8e98..789a7f5d901 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json @@ -50,7 +50,7 @@ "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Mobile Safari/537.36" ] }, - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json new file mode 100644 index 00000000000..ddf37962054 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + } + ], + "currency": "NOK" + } + ] +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/size-check.json b/adapters/adnuntius/adnuntiustest/supplemental/size-check.json index c05428c123f..ca551380d4b 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/size-check.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/size-check.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/status-400.json b/adapters/adnuntius/adnuntiustest/supplemental/status-400.json index f8407b1de5b..84d51bd64f6 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/status-400.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/status-400.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json b/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json index 2e0f0afcbbd..0cb49b946c1 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json @@ -32,7 +32,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json new file mode 100644 index 00000000000..2c2dcac1575 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "ext":{ + "eids" : [ + { + "source": "idProvider", + "uids": [ + { "id": "userId", "atype": 1, "ext": { "stype": "ppuid" } } + ] + } + ] + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "metaData": { + "usi": "userId" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + } + ], + "currency": "NOK" + } + ] +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json index 4f109942b91..b3d2ecb7efc 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adtonos/adtonos.go b/adapters/adtonos/adtonos.go new file mode 100644 index 00000000000..dff60733955 --- /dev/null +++ b/adapters/adtonos/adtonos.go @@ -0,0 +1,142 @@ +package adtonos + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpointTemplate *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpointTemplate: template, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Invalid imp.ext for impression index %d. Error Infomation: %s", 0, err.Error()), + }} + } + var impExt openrtb_ext.ImpExtAdTonos + if err := json.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Invalid imp.ext.bidder for impression index %d. Error Infomation: %s", 0, err.Error()), + }} + } + + endpoint, err := a.buildEndpointURL(&impExt) + if err != nil { + return nil, []error{err} + } + + requestJson, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: endpoint, + Body: requestJson, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ImpExtAdTonos) (string, error) { + endpointParams := macros.EndpointTemplateParams{PublisherID: params.SupplierID} + return macros.ResolveMacros(a.endpointTemplate, endpointParams) +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errors []error + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForBid(seatBid.Bid[i], request.Imp) + if err != nil { + errors = append(errors, err) + continue + } + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, errors +} + +func getMediaTypeForBid(bid openrtb2.Bid, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) { + if bid.MType != 0 { + // If present, use explicit markup type annotation from the bidder: + switch bid.MType { + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + } + // As a fallback, guess markup type based on requested type - AdTonos is an audio company so we prioritize that. + for _, requestImp := range requestImps { + if requestImp.ID == bid.ImpID { + if requestImp.Audio != nil { + return openrtb_ext.BidTypeAudio, nil + } else if requestImp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } else { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", bid.ImpID), + } + } + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression: \"%s\"", bid.ImpID), + } +} diff --git a/adapters/adtonos/adtonos_test.go b/adapters/adtonos/adtonos_test.go new file mode 100644 index 00000000000..612e71783be --- /dev/null +++ b/adapters/adtonos/adtonos_test.go @@ -0,0 +1,30 @@ +package adtonos + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{ + Endpoint: "http://exchange.example.com/bid/{{.PublisherID}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "adtonostest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{ + Endpoint: "{{Malformed}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json new file mode 100644 index 00000000000..c9103e37ee4 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5, + "mtype": 3 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5, + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio.json b/adapters/adtonos/adtonostest/exemplary/simple-audio.json new file mode 100644 index 00000000000..61cac002660 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-video.json b/adapters/adtonos/adtonostest/exemplary/simple-video.json new file mode 100644 index 00000000000..f00089b4008 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-video.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "video-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..6fa20e3d3c3 --- /dev/null +++ b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "correct-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "correct-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["correct-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "unexpected-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression: \"unexpected-impression-id\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtonos/params_test.go b/adapters/adtonos/params_test.go new file mode 100644 index 00000000000..98fa85f7b0d --- /dev/null +++ b/adapters/adtonos/params_test.go @@ -0,0 +1,43 @@ +package adtonos + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"supplierId": ""}`, + `{"supplierId": "7YZxxxdJMSXWv7SwY"}`, +} + +var invalidParams = []string{ + `{"supplierId": 42}`, +} diff --git a/adapters/bidmatic/bidmatic.go b/adapters/bidmatic/bidmatic.go new file mode 100644 index 00000000000..950107ea0e0 --- /dev/null +++ b/adapters/bidmatic/bidmatic.go @@ -0,0 +1,206 @@ +package bidmatic + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type bidmaticImpExt struct { + Bidmatic openrtb_ext.ExtImpBidmatic `json:"bidmatic"` +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + totalImps := len(request.Imp) + errors := make([]error, 0, totalImps) + imp2source := make(map[int][]int) + + for i := 0; i < totalImps; i++ { + sourceId, err := validateImpression(&request.Imp[i]) + if err != nil { + errors = append(errors, err) + continue + } + + if _, ok := imp2source[sourceId]; !ok { + imp2source[sourceId] = make([]int, 0, totalImps-i) + } + + imp2source[sourceId] = append(imp2source[sourceId], i) + } + + totalReqs := len(imp2source) + if totalReqs == 0 { + return nil, errors + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + reqs := make([]*adapters.RequestData, 0, totalReqs) + + imps := request.Imp + request.Imp = make([]openrtb2.Imp, 0, len(imps)) + for sourceId, impIds := range imp2source { + request.Imp = request.Imp[:0] + + for i := 0; i < len(impIds); i++ { + request.Imp = append(request.Imp, imps[impIds[i]]) + } + + body, err := json.Marshal(request) + if err != nil { + errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err)) + return nil, errors + } + + reqs = append(reqs, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint + fmt.Sprintf("?source=%d", sourceId), + Body: body, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }) + } + + if len(reqs) == 0 { + return nil, errors + } + + return reqs, errors +} + +func (a *adapter) MakeBids(bidReq *openrtb2.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(httpRes) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(httpRes); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("error while decoding response, err: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponse() + var errors []error + + var impOK bool + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + + bid := sb.Bid[i] + + impOK = false + mediaType := openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + loop: + for _, imp := range bidReq.Imp { + if imp.ID == bid.ImpID { + + impOK = true + + switch { + case imp.Video != nil: + mediaType = openrtb_ext.BidTypeVideo + bid.MType = openrtb2.MarkupVideo + break loop + case imp.Banner != nil: + mediaType = openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + break loop + case imp.Audio != nil: + mediaType = openrtb_ext.BidTypeAudio + bid.MType = openrtb2.MarkupAudio + break loop + case imp.Native != nil: + mediaType = openrtb_ext.BidTypeNative + bid.MType = openrtb2.MarkupNative + break loop + } + } + } + + if !impOK { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: mediaType, + }) + } + } + + return bidResponse, errors +} + +func validateImpression(imp *openrtb2.Imp) (int, error) { + if len(imp.Ext) == 0 { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID), + } + } + + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), + } + } + + impExt := openrtb_ext.ExtImpBidmatic{} + err := json.Unmarshal(bidderExt.Bidder, &impExt) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), + } + } + + // common extension for all impressions + var impExtBuffer []byte + + impExtBuffer, err = json.Marshal(&bidmaticImpExt{ + Bidmatic: impExt, + }) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while marshaling impExt, err: %s", imp.ID, err), + } + } + + if impExt.BidFloor > 0 { + imp.BidFloor = impExt.BidFloor + } + + imp.Ext = impExtBuffer + + source, err := impExt.SourceId.Int64() // json.Unmarshal returns err if it isn't valid + if err != nil { + return 0, err + } + return int(source), nil +} + +// Builder builds a new instance of the bidmatic adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{endpoint: config.Endpoint}, nil +} diff --git a/adapters/bidmatic/bidmatic_test.go b/adapters/bidmatic/bidmatic_test.go new file mode 100644 index 00000000000..c6a31823223 --- /dev/null +++ b/adapters/bidmatic/bidmatic_test.go @@ -0,0 +1,23 @@ +package bidmatic + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderBidmatic, + config.Adapter{Endpoint: "http://adapter.bidmatic.io/pbs/ortb"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, + ) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "bidmatictest", bidder) +} diff --git a/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json new file mode 100644 index 00000000000..57f1215af43 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json new file mode 100644 index 00000000000..d3c41278231 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ] + }, + "bidfloor": 20, + "ext": { + "bidmatic": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 1, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-video.json b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json new file mode 100644 index 00000000000..a9bcb6a141e --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..b1f2f6ea510 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json new file mode 100644 index 00000000000..0607da05fb9 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, extImpBidder is empty", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..8154afed75f --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "source": "some string instead of int" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: invalid number literal, trying to unmarshal \"\\\"some string instead of int\\\"\" into Number", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..05679082aa3 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "SOME-WRONG-IMP-ID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json new file mode 100644 index 00000000000..ad09b32cd1a --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "error while decoding response, err: unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/params_test.go b/adapters/bidmatic/params_test.go new file mode 100644 index 00000000000..6bdc5f4339d --- /dev/null +++ b/adapters/bidmatic/params_test.go @@ -0,0 +1,64 @@ +package bidmatic + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/bidmatic.json +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.bidmatic +// TestValidParams makes sure that the bidmatic 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.BidderBidmatic, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected bidmatic params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the bidmatic schema rejects all the imp.ext fields we don't support. +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.BidderBidmatic, json.RawMessage(invalidParam)); err == nil { + ext := openrtb_ext.ExtImpBidmatic{} + err = json.Unmarshal([]byte(invalidParam), &ext) + if err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } + } +} + +var validParams = []string{ + `{"source":123}`, + `{"source":"123"}`, + `{"source":123,"placementId":1234}`, + `{"source":123,"siteId":4321}`, + `{"source":"123","siteId":0,"bidFloor":0}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"source":"qwerty"}`, + `{"source":"123","placementId":"123"}`, + `{"source":123, "placementId":"123", "siteId":"321"}`, +} diff --git a/adapters/bluesea/blueseatest/exemplary/site-banner.json b/adapters/bluesea/blueseatest/exemplary/site-banner.json new file mode 100644 index 00000000000..b3ca13f8d53 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + }, + "type":"banner" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/site-native.json b/adapters/bluesea/blueseatest/exemplary/site-native.json new file mode 100644 index 00000000000..b907ebdbea4 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-native.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + }, + "type":"native" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/site-video.json b/adapters/bluesea/blueseatest/exemplary/site-video.json new file mode 100644 index 00000000000..dd191105cd3 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-video.json @@ -0,0 +1,163 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + }, + "type":"video" + } + ] + } + ] +} diff --git a/adapters/connectad/connectad.go b/adapters/connectad/connectad.go index 5bf60106016..1617d3d66fd 100644 --- a/adapters/connectad/connectad.go +++ b/adapters/connectad/connectad.go @@ -138,7 +138,7 @@ func preprocess(request *openrtb2.BidRequest) []error { } func addImpInfo(imp *openrtb2.Imp, secure *int8, cadExt *openrtb_ext.ExtImpConnectAd) { - imp.TagID = strconv.Itoa(cadExt.SiteID) + imp.TagID = strconv.Itoa(int(cadExt.SiteID)) imp.Secure = secure if cadExt.Bidfloor != 0 { diff --git a/adapters/copper6ssp/copper6ssp.go b/adapters/copper6ssp/copper6ssp.go new file mode 100644 index 00000000000..57ec6bcc17b --- /dev/null +++ b/adapters/copper6ssp/copper6ssp.go @@ -0,0 +1,157 @@ +package copper6ssp + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + Copper6sspBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var copper6sspExt openrtb_ext.ImpExtCopper6ssp + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, err) + continue + } + if err := json.Unmarshal(bidderExt.Bidder, &copper6sspExt); err != nil { + errs = append(errs, err) + continue + } + + impExt := reqBodyExt{Copper6sspBidderExt: reqBodyExtBidder{}} + + if copper6sspExt.PlacementID != "" { + impExt.Copper6sspBidderExt.PlacementID = copper6sspExt.PlacementID + impExt.Copper6sspBidderExt.Type = "publisher" + } else if copper6sspExt.EndpointID != "" { + impExt.Copper6sspBidderExt.EndpointID = copper6sspExt.EndpointID + impExt.Copper6sspBidderExt.Type = "network" + } + + finalImpExt, err := json.Marshal(impExt) + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].Ext = finalImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + errs = append(errs, err) + continue + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + + if len(adapterRequests) == 0 { + return nil, errs + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if len(response.Cur) != 0 { + bidResponse.Currency = response.Cur + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidType(seatBid.Bid[i]) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + // determinate media type by bid response field mtype + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + return "", fmt.Errorf("could not define media type for impression: %s", bid.ImpID) +} diff --git a/adapters/copper6ssp/copper6ssp_test.go b/adapters/copper6ssp/copper6ssp_test.go new file mode 100644 index 00000000000..25bd2dbc67a --- /dev/null +++ b/adapters/copper6ssp/copper6ssp_test.go @@ -0,0 +1,20 @@ +package copper6ssp + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderCopper6ssp, config.Adapter{ + Endpoint: "https://example.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "copper6ssptest", bidder) +} diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/endpointId.json b/adapters/copper6ssp/copper6ssptest/exemplary/endpointId.json new file mode 100644 index 00000000000..3dc82f836da --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/endpointId.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/multi-format.json b/adapters/copper6ssp/copper6ssptest/exemplary/multi-format.json new file mode 100644 index 00000000000..9e897a9a1bb --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/multi-format.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/multi-imp.json b/adapters/copper6ssp/copper6ssptest/exemplary/multi-imp.json new file mode 100644 index 00000000000..63828352744 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/multi-imp.json @@ -0,0 +1,253 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + }, + { + "id": "test-imp-id2", + "tagid": "test2", + "banner": { + "format": [ + { + "w": 3000, + "h": 2500 + }, + { + "w": 3000, + "h": 6000 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id2", + "tagid": "test2", + "banner": { + "format": [ + { + "w": 3000, + "h": 2500 + }, + { + "w": 3000, + "h": 6000 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test2", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id2"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id2", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 3000, + "h": 2500, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id2", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 3000, + "h": 2500, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/simple-banner.json b/adapters/copper6ssp/copper6ssptest/exemplary/simple-banner.json new file mode 100644 index 00000000000..3bff225709d --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/simple-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/simple-native.json b/adapters/copper6ssp/copper6ssptest/exemplary/simple-native.json new file mode 100644 index 00000000000..63b4a5c824a --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/simple-native.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/simple-video.json b/adapters/copper6ssp/copper6ssptest/exemplary/simple-video.json new file mode 100644 index 00000000000..60156082dc7 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/simple-video.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/exemplary/simple-web-banner.json b/adapters/copper6ssp/copper6ssptest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..3ff97037a82 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/exemplary/simple-web-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/bad_media_type.json b/adapters/copper6ssp/copper6ssptest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..3b61edd137d --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/bad_media_type.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": {} + } + ], + "seat": "copper6ssp" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "could not define media type for impression: test-imp-id", + "comparison": "literal" + } + ] +} diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/bad_response.json b/adapters/copper6ssp/copper6ssptest/supplemental/bad_response.json new file mode 100644 index 00000000000..cd4169c9974 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-bidder-param.json b/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-bidder-param.json new file mode 100644 index 00000000000..fbe564b6a26 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-bidder-param.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": [] + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal array into Go struct field ImpExtCopper6ssp.endpointId of type string", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-imp-ext.json b/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-imp-ext.json new file mode 100644 index 00000000000..9d6710efe37 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/no-valid-imp-ext.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": "invalid" + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/status-204.json b/adapters/copper6ssp/copper6ssptest/supplemental/status-204.json new file mode 100644 index 00000000000..7dd1c65fd36 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/status-204.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/copper6ssp/copper6ssptest/supplemental/status-not-200.json b/adapters/copper6ssp/copper6ssptest/supplemental/status-not-200.json new file mode 100644 index 00000000000..743f2996260 --- /dev/null +++ b/adapters/copper6ssp/copper6ssptest/supplemental/status-not-200.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/copper6ssp/params_test.go b/adapters/copper6ssp/params_test.go new file mode 100644 index 00000000000..f067a5f9d5c --- /dev/null +++ b/adapters/copper6ssp/params_test.go @@ -0,0 +1,47 @@ +package copper6ssp + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderCopper6ssp, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderCopper6ssp, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/displayio/displayio.go b/adapters/displayio/displayio.go new file mode 100644 index 00000000000..b54998553b3 --- /dev/null +++ b/adapters/displayio/displayio.go @@ -0,0 +1,188 @@ +package displayio + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +type reqDioExt struct { + UserSession string `json:"userSession,omitempty"` + PlacementId string `json:"placementId"` + InventoryId string `json:"inventoryId"` +} + +func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + + result := make([]*adapters.RequestData, 0, len(request.Imp)) + errs := make([]error, 0, len(request.Imp)) + + for _, impression := range request.Imp { + var requestExt map[string]interface{} + + if impression.BidFloorCur == "" || impression.BidFloor == 0 { + impression.BidFloorCur = "USD" + } else if impression.BidFloorCur != "USD" { + convertedValue, err := requestInfo.ConvertCurrency(impression.BidFloor, impression.BidFloorCur, "USD") + + if err != nil { + errs = append(errs, err) + continue + } + + impression.BidFloor = convertedValue + impression.BidFloorCur = "USD" + } + + if len(impression.Ext) == 0 { + errs = append(errs, errors.New("impression extensions required")) + continue + } + + var bidderExt adapters.ExtImpBidder + err := json.Unmarshal(impression.Ext, &bidderExt) + + if err != nil { + errs = append(errs, err) + continue + } + + var impressionExt openrtb_ext.ExtImpDisplayio + err = json.Unmarshal(bidderExt.Bidder, &impressionExt) + if err != nil { + errs = append(errs, err) + continue + } + + dioExt := reqDioExt{PlacementId: impressionExt.PlacementId, InventoryId: impressionExt.InventoryId} + + requestCopy := *request + + err = json.Unmarshal(requestCopy.Ext, &requestExt) + if err != nil { + requestExt = make(map[string]interface{}) + } + + requestExt["displayio"] = dioExt + + requestCopy.Ext, err = json.Marshal(requestExt) + if err != nil { + errs = append(errs, err) + continue + } + + requestCopy.Imp = []openrtb2.Imp{impression} + body, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + url, err := adapter.buildEndpointURL(&impressionExt) + if err != nil { + return nil, []error{err} + } + + result = append(result, &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: body, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp), + }) + } + + if len(result) == 0 { + return nil, errs + } + return result, errs +} + +// MakeBids translates Displayio bid response to prebid-server specific format +func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + + if err := json.Unmarshal(responseData.Body, &bidResp); err != nil { + msg := fmt.Sprintf("Bad server response: %d", err) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + + if len(bidResp.SeatBid) != 1 { + msg := fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid)) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + + var errs []error + bidResponse := adapters.NewBidderResponse() + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidType, err := getBidMediaTypeFromMtype(&sb.Bid[i]) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + + return bidResponse, errs +} + +func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) { + endpoint, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: endpoint, + } + return bidder, nil +} + +func getBidMediaTypeFromMtype(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("unexpected media type for bid: %s", bid.ImpID) + } +} + +func (adapter *adapter) buildEndpointURL(params *openrtb_ext.ExtImpDisplayio) (string, error) { + endpointParams := macros.EndpointTemplateParams{PublisherID: params.PublisherId} + return macros.ResolveMacros(adapter.endpoint, endpointParams) +} diff --git a/adapters/escalax/escalax.go b/adapters/escalax/escalax.go new file mode 100644 index 00000000000..87cec5e17a9 --- /dev/null +++ b/adapters/escalax/escalax.go @@ -0,0 +1,162 @@ +package escalax + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return headers +} + +func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { + escalaxExt, err := getImpressionExt(&openRTBRequest.Imp[0]) + if err != nil { + return nil, []error{err} + } + + openRTBRequest.Imp[0].Ext = nil + + url, err := a.buildEndpointURL(escalaxExt) + if err != nil { + return nil, []error{err} + } + + reqJSON, err := json.Marshal(openRTBRequest) + if err != nil { + return nil, []error{err} + } + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Body: reqJSON, + Uri: url, + Headers: getHeaders(openRTBRequest), + ImpIDs: openrtb_ext.GetImpIDs(openRTBRequest.Imp), + }}, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtEscalax, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing escalaxExt - " + err.Error(), + } + } + var escalaxExt openrtb_ext.ExtEscalax + if err := json.Unmarshal(bidderExt.Bidder, &escalaxExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing bidderExt - " + err.Error(), + } + } + + return &escalaxExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtEscalax) (string, error) { + endpointParams := macros.EndpointTemplateParams{AccountID: params.AccountID, SourceId: params.SourceID} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil { + return nil, []error{err} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + var bidsArray []*adapters.TypedBid + + for _, sb := range bidResp.SeatBid { + for idx, bid := range sb.Bid { + bidType, err := determineImpressionMediaType(bid) + if err != nil { + return nil, []error{err} + } + + bidsArray = append(bidsArray, &adapters.TypedBid{ + Bid: &sb.Bid[idx], + BidType: bidType, + }) + } + } + + bidResponse.Bids = bidsArray + return bidResponse, nil +} + +func determineImpressionMediaType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("unsupported MType %d", bid.MType), + } + } +} diff --git a/adapters/escalax/escalax_test.go b/adapters/escalax/escalax_test.go new file mode 100644 index 00000000000..dd1200ff61b --- /dev/null +++ b/adapters/escalax/escalax_test.go @@ -0,0 +1,28 @@ +package escalax + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderEscalax, config.Adapter{ + Endpoint: "http://bidder_us.escalax.io/?partner={{.SourceId}}&token={{.AccountID}}&type=pbs"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "escalaxtest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderEscalax, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/escalax/escalaxtest/exemplary/banner-app.json b/adapters/escalax/escalaxtest/exemplary/banner-app.json new file mode 100644 index 00000000000..e76ca1e291c --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/banner-app.json @@ -0,0 +1,155 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "1" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/exemplary/banner-web.json b/adapters/escalax/escalaxtest/exemplary/banner-web.json new file mode 100644 index 00000000000..28432e46f76 --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/banner-web.json @@ -0,0 +1,203 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId", + "host": "host" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId", + "host": "host" + } + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id1", + "some-impression-id2" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/exemplary/native-app.json b/adapters/escalax/escalaxtest/exemplary/native-app.json new file mode 100644 index 00000000000..27d47d9d19b --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/native-app.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "type": "native", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/exemplary/native-web.json b/adapters/escalax/escalaxtest/exemplary/native-web.json new file mode 100644 index 00000000000..4480d14d11e --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/native-web.json @@ -0,0 +1,138 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2607:fb90:f27:4512:d800:cb23:a603:e245" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver": "1.1", + "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/exemplary/video-app.json b/adapters/escalax/escalaxtest/exemplary/video-app.json new file mode 100644 index 00000000000..2c23fb27aa3 --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/video-app.json @@ -0,0 +1,164 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2 + } + ], + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/exemplary/video-web.json b/adapters/escalax/escalaxtest/exemplary/video-web.json new file mode 100644 index 00000000000..4204767f794 --- /dev/null +++ b/adapters/escalax/escalaxtest/exemplary/video-web.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/bad_media_type.json b/adapters/escalax/escalaxtest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..b98d680cb2e --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/bad_media_type.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "1" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "test-imp-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 0 + } + ], + "type": "banner", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unsupported MType 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/empty-seatbid-array.json b/adapters/escalax/escalaxtest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..a7b8c12194e --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/empty-seatbid-array.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "mockResponse": { + "status": 200, + "body": "invalid response" + }, + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/invalid-bidder-ext-object.json b/adapters/escalax/escalaxtest/supplemental/invalid-bidder-ext-object.json new file mode 100644 index 00000000000..96a3037ced0 --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/invalid-bidder-ext-object.json @@ -0,0 +1,33 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt - json: cannot unmarshal string into Go value of type openrtb_ext.ExtEscalax", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": "wrongBidderExt" + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/invalid-ext-object.json b/adapters/escalax/escalaxtest/supplemental/invalid-ext-object.json new file mode 100644 index 00000000000..f3725c1005d --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/invalid-ext-object.json @@ -0,0 +1,31 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing escalaxExt - json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "wrongEscalaxExt" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/invalid-response.json b/adapters/escalax/escalaxtest/supplemental/invalid-response.json new file mode 100644 index 00000000000..f511eafa980 --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/invalid-response.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad Server Response", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/status-code-bad-request.json b/adapters/escalax/escalaxtest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..d6537aac95e --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/status-code-bad-request.json @@ -0,0 +1,96 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/escalaxtest/supplemental/status-code-other-error.json b/adapters/escalax/escalaxtest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..00d618e5b29 --- /dev/null +++ b/adapters/escalax/escalaxtest/supplemental/status-code-other-error.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "accountId": "accountId", + "sourceId": "sourceId" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://bidder_us.escalax.io/?partner=sourceId&token=accountId&type=pbs", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 306 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/escalax/params_test.go b/adapters/escalax/params_test.go new file mode 100644 index 00000000000..a61af936055 --- /dev/null +++ b/adapters/escalax/params_test.go @@ -0,0 +1,52 @@ +package escalax + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +var validParams = []string{ + `{ "sourceId": "someSourceId", "accountId": "0800fc577294" }`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `9`, + `1.2`, + `[]`, + `{}`, + `{ "accountId": "", "sourceId": "" }`, + `{ "accountId": true, "sourceId": true }`, + `{ "accountId": 123, "sourceId": 123 }`, + `{ "accountId": null, "sourceId": null }`, +} + +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.BidderEscalax, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Escalax 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.BidderEscalax, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json index e6838240ca4..18a1dbdbdef 100644 --- a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json +++ b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json @@ -72,7 +72,8 @@ "Componentid": [ "prebid-go" ] - } + }, + "impIDs":["imp-1","imp-2"] }, "mockResponse": { "status": 200, @@ -88,7 +89,10 @@ "adid": "7857", "adm": "", "cid": "4001", - "crid": "7857" + "crid": "7857", + "adomain":["freewheel.com"], + "cat": ["IAB10"], + "dur": 14 }, { "id": "12346_freewheelssp-test_2", @@ -97,7 +101,9 @@ "adid": "7933", "adm": "", "cid": "3476", - "crid": "7933" + "crid": "7933", + "adomain":["freewheel.com"], + "dur":10 } ], "seat": "freewheelsspTv" @@ -122,9 +128,16 @@ "adid": "7857", "adm": "", "cid": "4001", - "crid": "7857" + "crid": "7857", + "adomain":["freewheel.com"], + "cat": ["IAB10"], + "dur": 14 }, - "type": "video" + "type": "video", + "video" : { + "duration" : 14, + "primary_category": "IAB10" + } }, { "bid": { @@ -134,9 +147,15 @@ "adid": "7933", "adm": "", "cid": "3476", - "crid": "7933" + "crid": "7933", + "adomain":["freewheel.com"], + "dur":10 }, - "type": "video" + "type": "video", + "video" : { + "duration" : 10, + "primary_category": "" + } } ] } diff --git a/adapters/gumgum/gumgum.go b/adapters/gumgum/gumgum.go index 0fb06d3c6bf..57ffae5385a 100644 --- a/adapters/gumgum/gumgum.go +++ b/adapters/gumgum/gumgum.go @@ -160,12 +160,8 @@ func preprocess(imp *openrtb2.Imp) (*openrtb_ext.ExtImpGumGum, error) { } if imp.Video != nil { - err := validateVideoParams(imp.Video) - if err != nil { - return nil, err - } - if gumgumExt.IrisID != "" { + var err error videoCopy := *imp.Video videoExt := openrtb_ext.ExtImpGumGumVideo{IrisID: gumgumExt.IrisID} videoCopy.Ext, err = json.Marshal(&videoExt) @@ -220,15 +216,6 @@ func getMediaTypeForImpID(impID string, imps []openrtb2.Imp) openrtb_ext.BidType return openrtb_ext.BidTypeVideo } -func validateVideoParams(video *openrtb2.Video) (err error) { - if video.W == nil || *video.W == 0 || video.H == nil || *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 -} - // Builder builds a new instance of the GumGum adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &adapter{ diff --git a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json deleted file mode 100644 index b2475cd7bb4..00000000000 --- a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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/gumgum/gumgumtest/supplemental/video-missing-size.json b/adapters/gumgum/gumgumtest/supplemental/video-missing-size.json deleted file mode 100644 index 1e4afe167ea..00000000000 --- a/adapters/gumgum/gumgumtest/supplemental/video-missing-size.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": [ - "video/mp4" - ], - "minduration": 1, - "maxduration": 2, - "protocols": [ - 1, - 2 - ], - "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/gumgum/gumgumtest/supplemental/video-partial-size.json b/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json index 3c9727a1a9c..ce43a93b145 100644 --- a/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json +++ b/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json @@ -27,10 +27,79 @@ } ] }, - "expectedMakeRequestsErrors": [ + "httpCalls": [ { - "value": "Invalid or missing video field(s)", - "comparison": "literal" + "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, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + } + ] + } + ] + } + } + } +], +"expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + }, + "type": "video" + } + ] } - ] +] } \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json b/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json index d3d4b427120..4fc7dc3ce77 100644 --- a/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json +++ b/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json @@ -28,10 +28,80 @@ } ] }, - "expectedMakeRequestsErrors": [ + "httpCalls": [ { - "value": "Invalid or missing video field(s)", - "comparison": "literal" + "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": 0, + "h": 0, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + } + ] + } + ] + } + } + } +], +"expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + }, + "type": "video" + } + ] } - ] +] } \ No newline at end of file diff --git a/adapters/improvedigital/improvedigital.go b/adapters/improvedigital/improvedigital.go index 3fd514be86d..4c7ca6328cf 100644 --- a/adapters/improvedigital/improvedigital.go +++ b/adapters/improvedigital/improvedigital.go @@ -16,12 +16,9 @@ import ( ) const ( - isRewardedInventory = "is_rewarded_inventory" - stateRewardedInventoryEnable = "1" - consentProvidersSettingsInputKey = "ConsentedProvidersSettings" - consentProvidersSettingsOutKey = "consented_providers_settings" - consentedProvidersKey = "consented_providers" - publisherEndpointParam = "{PublisherId}" + isRewardedInventory = "is_rewarded_inventory" + stateRewardedInventoryEnable = "1" + publisherEndpointParam = "{PublisherId}" ) type ImprovedigitalAdapter struct { @@ -75,17 +72,6 @@ func (a *ImprovedigitalAdapter) makeRequest(request openrtb2.BidRequest, imp ope request.Imp = []openrtb2.Imp{imp} - userExtAddtlConsent, err := a.getAdditionalConsentProvidersUserExt(request) - if err != nil { - return nil, err - } - - if len(userExtAddtlConsent) > 0 { - userCopy := *request.User - userCopy.Ext = userExtAddtlConsent - request.User = &userCopy - } - reqJSON, err := json.Marshal(request) if err != nil { return nil, err @@ -254,69 +240,6 @@ func isMultiFormatImp(imp openrtb2.Imp) bool { return formatCount > 1 } -// This method responsible to clone request and convert additional consent providers string to array when additional consent provider found -func (a *ImprovedigitalAdapter) getAdditionalConsentProvidersUserExt(request openrtb2.BidRequest) ([]byte, error) { - var cpStr string - - // If user/user.ext not defined, no need to parse additional consent - if request.User == nil || request.User.Ext == nil { - return nil, nil - } - - // Start validating additional consent - // Check key exist user.ext.ConsentedProvidersSettings - var userExtMap = make(map[string]json.RawMessage) - if err := json.Unmarshal(request.User.Ext, &userExtMap); err != nil { - return nil, err - } - - cpsMapValue, cpsJSONFound := userExtMap[consentProvidersSettingsInputKey] - if !cpsJSONFound { - return nil, nil - } - - // Check key exist user.ext.ConsentedProvidersSettings.consented_providers - var cpMap = make(map[string]json.RawMessage) - if err := json.Unmarshal(cpsMapValue, &cpMap); err != nil { - return nil, err - } - - cpMapValue, cpJSONFound := cpMap[consentedProvidersKey] - if !cpJSONFound { - return nil, nil - } - // End validating additional consent - - // Trim enclosing quotes after casting json.RawMessage to string - consentStr := strings.Trim((string)(cpMapValue), "\"") - // Split by ~ and take only the second string (if exists) as the consented providers spec - var consentStrParts = strings.Split(consentStr, "~") - if len(consentStrParts) < 2 { - return nil, nil - } - cpStr = strings.TrimSpace(consentStrParts[1]) - if len(cpStr) == 0 { - return nil, nil - } - - // Prepare consent providers string - cpStr = fmt.Sprintf("[%s]", strings.Replace(cpStr, ".", ",", -1)) - cpMap[consentedProvidersKey] = json.RawMessage(cpStr) - - cpJSON, err := json.Marshal(cpMap) - if err != nil { - return nil, err - } - userExtMap[consentProvidersSettingsOutKey] = cpJSON - - extJson, err := json.Marshal(userExtMap) - if err != nil { - return nil, err - } - - return extJson, nil -} - func getImpExtWithRewardedInventory(imp openrtb2.Imp) ([]byte, error) { var ext = make(map[string]json.RawMessage) if err := json.Unmarshal(imp.Ext, &ext); err != nil { diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json deleted file mode 100644 index 24c4b813ff3..00000000000 --- a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "mockBidRequest": { - "id": "addtl-consent-request-id", - "site": { - "page": "https://good.site/url" - }, - "imp": [{ - "id": "test-imp-id", - "banner": { - "format": [{ - "w": 300, - "h": 250 - }] - }, - "ext": { - "bidder": { - "placementId": 13245 - } - } - }], - "user": { - "ext":{"consent":"ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"}} - } - }, - - "httpCalls": [{ - "expectedRequest": { - "uri": "http://localhost/pbs", - "body": { - "id": "addtl-consent-request-id", - "site": { - "page": "https://good.site/url" - }, - "imp": [{ - "id": "test-imp-id", - "banner": { - "format": [{ - "w": 300, - "h": 250 - }] - }, - "ext": { - "bidder": { - "placementId": 13245 - } - } - }], - "user": { - "ext": {"consent": "ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"},"consented_providers_settings": {"consented_providers": [10,20,90]} - } - } - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "addtl-consent-request-id", - "seatbid": [{ - "seat": "improvedigital", - "bid": [{ - "id": "randomid", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "12345678", - "adm": "some-test-ad", - "cid": "987", - "crid": "12345678", - "h": 250, - "w": 300 - }] - }], - "cur": "USD" - } - } - }], - - "expectedBidResponses": [{ - "currency": "USD", - "bids": [{ - "bid": { - "id": "randomid", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "12345678", - "cid": "987", - "crid": "12345678", - "w": 300, - "h": 250 - }, - "type": "banner" - }] - }] -} diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent.json b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent.json deleted file mode 100644 index b1b780f64ba..00000000000 --- a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "mockBidRequest": { - "id": "addtl-consent-request-id", - "site": { - "page": "https://good.site/url" - }, - "imp": [{ - "id": "test-imp-id", - "banner": { - "format": [{ - "w": 300, - "h": 250 - }] - }, - "ext": { - "bidder": { - "placementId": 13245 - } - } - }], - "user": { - "ext":{"consent":"ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90"}} - } - }, - - "httpCalls": [{ - "expectedRequest": { - "uri": "http://localhost/pbs", - "body": { - "id": "addtl-consent-request-id", - "site": { - "page": "https://good.site/url" - }, - "imp": [{ - "id": "test-imp-id", - "banner": { - "format": [{ - "w": 300, - "h": 250 - }] - }, - "ext": { - "bidder": { - "placementId": 13245 - } - } - }], - "user": { - "ext": {"consent": "ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90"},"consented_providers_settings": {"consented_providers": [10,20,90]} - } - } - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "addtl-consent-request-id", - "seatbid": [{ - "seat": "improvedigital", - "bid": [{ - "id": "randomid", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "12345678", - "adm": "some-test-ad", - "cid": "987", - "crid": "12345678", - "h": 250, - "w": 300 - }] - }], - "cur": "USD" - } - } - }], - - "expectedBidResponses": [{ - "currency": "USD", - "bids": [{ - "bid": { - "id": "randomid", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "12345678", - "cid": "987", - "crid": "12345678", - "w": 300, - "h": 250 - }, - "type": "banner" - }] - }] -} diff --git a/adapters/improvedigital/params_test.go b/adapters/improvedigital/params_test.go index 3767c0316a5..86c0611f162 100644 --- a/adapters/improvedigital/params_test.go +++ b/adapters/improvedigital/params_test.go @@ -36,7 +36,6 @@ func TestInvalidParams(t *testing.T) { var validParams = []string{ `{"placementId":13245}`, `{"placementId":13245, "size": {"w":16, "h":9}}`, - `{"publisherId":13245, "placementKey": "slotA"}`, `{"placementId":13245, "keyValues":{"target1":["foo"],"target2":["bar", "baz"]}}`, } @@ -56,5 +55,5 @@ var invalidParams = []string{ `{"placementId": "1"}`, `{"size": true}`, `{"placementId": true, "size":"1234567"}`, - `{"placementId":13245, "publisherId":13245, "placementKey": "slotA"}`, + `{"publisherId":13245, "placementKey": "slotA"}`, } diff --git a/adapters/inmobi/inmobi.go b/adapters/inmobi/inmobi.go index 3c20fdc5627..af6b9838832 100644 --- a/adapters/inmobi/inmobi.go +++ b/adapters/inmobi/inmobi.go @@ -76,7 +76,10 @@ func (a *InMobiAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR for _, sb := range serverBidResponse.SeatBid { for i := range sb.Bid { - mediaType := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + mediaType, err := getMediaTypeForImp(sb.Bid[i]) + if err != nil { + return nil, []error{err} + } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &sb.Bid[i], BidType: mediaType, @@ -117,18 +120,17 @@ func preprocess(imp *openrtb2.Imp) error { return nil } -func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType { - mediaType := openrtb_ext.BidTypeBanner - for _, imp := range imps { - if imp.ID == impId { - if imp.Video != nil { - mediaType = openrtb_ext.BidTypeVideo - } - if imp.Native != nil { - mediaType = openrtb_ext.BidTypeNative - } - break +func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unsupported mtype %d for bid %s", bid.MType, bid.ID), } } - return mediaType } diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json b/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json index 4345ef8ff66..a74d091fce6 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json @@ -74,7 +74,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -93,6 +94,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-native.json b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json index 3a5bfd38412..cef76b65c6f 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-native.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json @@ -72,7 +72,8 @@ "price": 2.0, "id": "1234", "adm": "native-json", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 4 } ] } @@ -91,6 +92,7 @@ "adm": "native-json", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 4, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-video.json b/adapters/inmobi/inmobitest/exemplary/simple-app-video.json index 20b3c0cc810..162b1b97418 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-video.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-video.json @@ -76,7 +76,8 @@ "price": 2.0, "id": "1234", "adm": " ", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 2 } ] } @@ -95,6 +96,7 @@ "adm": " ", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 2, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json b/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json index 131249ba8a1..493a47043b5 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json @@ -72,7 +72,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -91,6 +92,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-native.json b/adapters/inmobi/inmobitest/exemplary/simple-web-native.json new file mode 100644 index 00000000000..25313573ea8 --- /dev/null +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-native.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1716021069867" + } + }, + "native": { + "request": "{\"ver\":\"1.2\",\"context\":2,\"contextsubtype\":20,\"plcmttype\":11,\"plcmtcnt\":1,\"aurlsupport\":1,\"durlsupport\":1,\"assets\":[{\"id\":123,\"required\":1,\"title\":{\"len\":140}},{\"id\":128,\"required\":0,\"img\":{\"wmin\":836,\"hmin\":627,\"type\":3}}]}" + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1716021069867" + } + }, + "native": { + "request": "{\"ver\":\"1.2\",\"context\":2,\"contextsubtype\":20,\"plcmttype\":11,\"plcmtcnt\":1,\"aurlsupport\":1,\"durlsupport\":1,\"assets\":[{\"id\":123,\"required\":1,\"title\":{\"len\":140}},{\"id\":128,\"required\":0,\"img\":{\"wmin\":836,\"hmin\":627,\"type\":3}}]}" + }, + "id": "imp-id" + } + ] + }, + "impIDs":["imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": "native-json", + "impid": "imp-id", + "mtype": 4 + } + ] + } + ] + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "1234", + "impid": "imp-id", + "price": 2.0, + "adm": "native-json", + "crid": "123456789", + "nurl": "https://some.event.url/params", + "mtype": 4, + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + } + }, + "type": "native" + }] + }] +} diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-video.json b/adapters/inmobi/inmobitest/exemplary/simple-web-video.json index 3aed605f416..be27041e691 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-web-video.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-video.json @@ -74,7 +74,8 @@ "price": 2.0, "id": "1234", "adm": " ", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 2 } ] } @@ -93,6 +94,7 @@ "adm": " ", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 2, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json index 514f86817c9..9913e6431d0 100644 --- a/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json +++ b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json @@ -80,7 +80,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -99,6 +100,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json b/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json new file mode 100644 index 00000000000..af2192836b0 --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1621323101291" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1621323101291" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + }, + "impIDs":["imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": " ", + "impid": "imp-id", + "mtype": 0 + } + ] + } + ] + } + } + }], + + "expectedBidResponses":[], + "expectedMakeBidsErrors":[ + { + "value":"Unsupported mtype 0 for bid 1234", + "comparison":"literal" + } + ] +} + + diff --git a/adapters/lemmadigital/lemmadigital_test.go b/adapters/lemmadigital/lemmadigital_test.go index e0062c0b565..cf507f34504 100644 --- a/adapters/lemmadigital/lemmadigital_test.go +++ b/adapters/lemmadigital/lemmadigital_test.go @@ -10,7 +10,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderLemmadigital, config.Adapter{ - Endpoint: "https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{.PublisherID}}&aid={{.AdUnit}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "https://test.lemmaurl.com/lemma/servad?src=prebid&pid={{.PublisherID}}&aid={{.AdUnit}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json index a478380e394..eab7fe5366b 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json @@ -32,7 +32,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json index e051b54ff95..ce160944122 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json @@ -49,7 +49,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json index 63bab75b674..97214b77960 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json index c88e0fa7861..73c1f42f44c 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json @@ -42,7 +42,7 @@ }, "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "app": { "bundle": "com.ld.test", diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json index 036d35fb345..cc0f393ad86 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json index 37ebea4b7be..ad25d3d2cba 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json index 7c4813df43a..e800577d5c6 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json index 047dc4efd83..ee13425f87a 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/melozen/melozen.go b/adapters/melozen/melozen.go new file mode 100644 index 00000000000..cb76274865b --- /dev/null +++ b/adapters/melozen/melozen.go @@ -0,0 +1,185 @@ +package melozen + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpointTemplate *template.Template +} + +// Builder builds a new instance of the MeloZen adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpointTemplate: template, + } + + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errors []error + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + // Extract Melozen Params + var strImpExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &strImpExt); err != nil { + errors = append(errors, err) + continue + } + var strImpParams openrtb_ext.ImpExtMeloZen + if err := json.Unmarshal(strImpExt.Bidder, &strImpParams); err != nil { + errors = append(errors, err) + continue + } + + url, err := macros.ResolveMacros(a.endpointTemplate, macros.EndpointTemplateParams{PublisherID: strImpParams.PubId}) + if err != nil { + errors = append(errors, err) + continue + } + // Convert Floor into USD + if imp.BidFloor > 0 && imp.BidFloorCur != "" && !strings.EqualFold(imp.BidFloorCur, "USD") { + convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD") + if err != nil { + errors = append(errors, err) + continue + } + imp.BidFloorCur = "USD" + imp.BidFloor = convertedValue + } + + impressionsByMediaType, err := splitImpressionsByMediaType(&imp) + if err != nil { + errors = append(errors, err) + continue + } + + for _, impression := range impressionsByMediaType { + requestCopy.Imp = []openrtb2.Imp{impression} + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errors = append(errors, err) + continue + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: requestJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp), + } + requests = append(requests, requestData) + } + } + + return requests, errors +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{err} + } + + var bidReq openrtb2.BidRequest + if err := json.Unmarshal(requestData.Body, &bidReq); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidderResponse := adapters.NewBidderResponse() + var errors []error + for _, seatBid := range bidResp.SeatBid { + for i := range seatBid.Bid { + bid := &seatBid.Bid[i] + bidType, err := getMediaTypeForBid(*bid) + if err != nil { + errors = append(errors, err) + continue + } + + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ + BidType: bidType, + Bid: bid, + }) + } + } + return bidderResponse, errors +} + +func splitImpressionsByMediaType(impression *openrtb2.Imp) ([]openrtb2.Imp, error) { + if impression.Banner == nil && impression.Native == nil && impression.Video == nil { + return nil, &errortypes.BadInput{Message: "Invalid MediaType. MeloZen only supports Banner, Video and Native."} + } + + impressions := make([]openrtb2.Imp, 0, 2) + + if impression.Banner != nil { + impCopy := *impression + impCopy.Video = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Video != nil { + impCopy := *impression + impCopy.Banner = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Native != nil { + impCopy := *impression + impCopy.Banner = nil + impCopy.Video = nil + impressions = append(impressions, impCopy) + } + + return impressions, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse bid mediatype for impression \"%s\"", bid.ImpID), + } +} diff --git a/adapters/melozen/melozen_test.go b/adapters/melozen/melozen_test.go new file mode 100644 index 00000000000..0191ab73182 --- /dev/null +++ b/adapters/melozen/melozen_test.go @@ -0,0 +1,30 @@ +package melozen + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + + bidder, buildErr := Builder(openrtb_ext.BidderMeloZen, config.Adapter{ + Endpoint: "https://example.com/rtb/v2/bid?publisher_id={{.PublisherID}}", + }, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "melozentest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderMeloZen, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/melozen/melozentest/exemplary/app-banner.json b/adapters/melozen/melozentest/exemplary/app-banner.json new file mode 100644 index 00000000000..6cdb82bf5ad --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs":["banner-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/app-native.json b/adapters/melozen/melozentest/exemplary/app-native.json new file mode 100644 index 00000000000..f93abd44bea --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-native.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ] + }, + "impIDs":["native-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-native", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/app-video.json b/adapters/melozen/melozentest/exemplary/app-video.json new file mode 100644 index 00000000000..3d913c43e44 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-video.json @@ -0,0 +1,137 @@ +{ + "mockBidRequest": { + "id": "app-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "app-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs": [ + "video-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "app-video", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "app-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "app-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/multi-imps.json b/adapters/melozen/melozentest/exemplary/multi-imps.json new file mode 100644 index 00000000000..916c74cb685 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/multi-imps.json @@ -0,0 +1,239 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-1", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "id": "banner-imp-id-2", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-1", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["banner-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id-1", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-2", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["banner-imp-id-2"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id-2", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id-1", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id-2", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/melozen/melozentest/exemplary/web-banner.json b/adapters/melozen/melozentest/exemplary/web-banner.json new file mode 100644 index 00000000000..0439baa1033 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/web-banner.json @@ -0,0 +1,138 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "baner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 0, + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "baner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs": [ + "baner-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "baner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "baner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/web-video.json b/adapters/melozen/melozentest/exemplary/web-video.json new file mode 100644 index 00000000000..b4f179bdc55 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/web-video.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "web-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": ["video/mp4"], + "placement": 1 + } + } + ], + "test": 0, + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": ["video/mp4"], + "placement": 1 + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["video-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-video", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/bad-media-type-request.json b/adapters/melozen/melozentest/supplemental/bad-media-type-request.json new file mode 100644 index 00000000000..f6c17a70b8f --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/bad-media-type-request.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "unsupported-request", + "imp": [ + { + "id": "unsupported-imp", + "unupported": { + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "site": { + "id": "siteID" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid MediaType. MeloZen only supports Banner, Video and Native.", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/no-fill.json b/adapters/melozen/melozentest/supplemental/no-fill.json new file mode 100644 index 00000000000..7dd600a72b6 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/no-fill.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs": [ + "banner-imp-id" + ] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/response-status-400.json b/adapters/melozen/melozentest/supplemental/response-status-400.json new file mode 100644 index 00000000000..969875b86ec --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/response-status-400.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs": [ + "banner-imp-id" + ] + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/response-status-not-200.json b/adapters/melozen/melozentest/supplemental/response-status-not-200.json new file mode 100644 index 00000000000..9b26ee58091 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/response-status-not-200.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json b/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json new file mode 100644 index 00000000000..b6a1c1f7268 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ] + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "another-imp-id", + "price": 3.5, + "w": 900, + "h": 250, + "ext": {} + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to parse bid mediatype for impression \"another-imp-id\"", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/params_test.go b/adapters/melozen/params_test.go new file mode 100644 index 00000000000..7e1be7f0db0 --- /dev/null +++ b/adapters/melozen/params_test.go @@ -0,0 +1,50 @@ +package melozen + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the JSON schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderMeloZen, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the JSON schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderMeloZen, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"pubId": "12345"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"pubId": ""}`, + `{"pubId": 12345}`, +} diff --git a/adapters/missena/missena.go b/adapters/missena/missena.go new file mode 100644 index 00000000000..93d4c2ba1cb --- /dev/null +++ b/adapters/missena/missena.go @@ -0,0 +1,215 @@ +package missena + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type MissenaAdRequest struct { + RequestId string `json:"request_id"` + Timeout int `json:"timeout"` + Referer string `json:"referer"` + RefererCanonical string `json:"referer_canonical"` + GDPRConsent string `json:"consent_string"` + GDPR bool `json:"consent_required"` + Placement string `json:"placement"` + TestMode string `json:"test"` +} + +type MissenaBidServerResponse struct { + Ad string `json:"ad"` + Cpm float64 `json:"cpm"` + Currency string `json:"currency"` + RequestId string `json:"requestId"` +} + +type MissenaInternalParams struct { + ApiKey string + RequestId string + Timeout int + Referer string + RefererCanonical string + GDPRConsent string + GDPR bool + Placement string + TestMode string +} + +type MissenaAdapter struct { + EndpointTemplate *template.Template +} + +// Builder builds a new instance of the Foo adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) makeRequest(missenaParams MissenaInternalParams, reqInfo *adapters.ExtraRequestInfo, impID string, request *openrtb2.BidRequest) (*adapters.RequestData, error) { + url := a.endpoint + "?t=" + missenaParams.ApiKey + + missenaRequest := MissenaAdRequest{ + RequestId: request.ID, + Timeout: 2000, + Referer: request.Site.Page, + RefererCanonical: request.Site.Domain, + GDPRConsent: missenaParams.GDPRConsent, + GDPR: missenaParams.GDPR, + Placement: missenaParams.Placement, + TestMode: missenaParams.TestMode, + } + + body, errm := json.Marshal(missenaRequest) + if errm != nil { + return nil, errm + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + if request.Device != nil { + headers.Add("User-Agent", request.Device.UA) + if request.Device.IP != "" { + headers.Add("X-Forwarded-For", request.Device.IP) + } else if request.Device.IPv6 != "" { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + } + if request.Site != nil { + headers.Add("Referer", request.Site.Page) + } + + return &adapters.RequestData{ + Method: "POST", + Uri: url, + Headers: headers, + Body: body, + ImpIDs: []string{impID}, + }, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + var httpRequests []*adapters.RequestData + var errors []error + gdprApplies, consentString := readGDPR(request) + + missenaInternalParams := MissenaInternalParams{ + GDPR: gdprApplies, + GDPRConsent: consentString, + } + + for _, imp := range request.Imp { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: "Error parsing bidderExt object", + }) + continue + } + + var missenaExt openrtb_ext.ExtImpMissena + if err := json.Unmarshal(bidderExt.Bidder, &missenaExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: "Error parsing missenaExt parameters", + }) + continue + } + + missenaInternalParams.ApiKey = missenaExt.ApiKey + missenaInternalParams.Placement = missenaExt.Placement + missenaInternalParams.TestMode = missenaExt.TestMode + + newHttpRequest, err := a.makeRequest(missenaInternalParams, requestInfo, imp.ID, request) + if err != nil { + errors = append(errors, err) + continue + } + + httpRequests = append(httpRequests, newHttpRequest) + + break + } + + return httpRequests, errors +} + +func readGDPR(request *openrtb2.BidRequest) (bool, string) { + consentString := "" + if request.User != nil { + var extUser openrtb_ext.ExtUser + if err := json.Unmarshal(request.User.Ext, &extUser); err == nil { + consentString = extUser.Consent + } + } + gdprApplies := false + var extRegs openrtb_ext.ExtRegs + if request.Regs != nil { + if err := json.Unmarshal(request.Regs.Ext, &extRegs); err == nil { + if extRegs.GDPR != nil { + gdprApplies = (*extRegs.GDPR == 1) + } + } + } + return gdprApplies, consentString +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode == http.StatusBadRequest { + err := &errortypes.BadInput{ + Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.", + } + return nil, []error{err} + } + + if responseData.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode), + } + return nil, []error{err} + } + + var missenaResponse MissenaBidServerResponse + if err := json.Unmarshal(responseData.Body, &missenaResponse); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + bidResponse.Currency = missenaResponse.Currency + + responseBid := &openrtb2.Bid{ + ID: request.ID, + Price: float64(missenaResponse.Cpm), + ImpID: request.Imp[0].ID, + AdM: missenaResponse.Ad, + CrID: missenaResponse.RequestId, + } + + b := &adapters.TypedBid{ + Bid: responseBid, + BidType: openrtb_ext.BidTypeBanner, + } + + bidResponse.Bids = append(bidResponse.Bids, b) + + return bidResponse, nil +} diff --git a/adapters/missena/missena_test.go b/adapters/missena/missena_test.go new file mode 100644 index 00000000000..2b13bf085db --- /dev/null +++ b/adapters/missena/missena_test.go @@ -0,0 +1,21 @@ +package missena + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderMissena, config.Adapter{ + Endpoint: "http://example.com/"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "missenatest", bidder) +} diff --git a/adapters/missena/missenatest/exemplary/multiple-imps.json b/adapters/missena/missenatest/exemplary/multiple-imps.json new file mode 100644 index 00000000000..5b83f19ccd0 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/multiple-imps.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-1", + "test": "1" + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-2", + "test": "1" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": "abc" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement-1", + "test": "1" + }, + "impIDs":["test-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id-1", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json b/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json new file mode 100644 index 00000000000..ea240f82e09 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ipv6": "2001:0000:130F:0000:0000:09C0:876A:130B", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2001:0000:130F:0000:0000:09C0:876A:130B" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/simple-banner.json b/adapters/missena/missenatest/exemplary/simple-banner.json new file mode 100644 index 00000000000..74ff3abfd57 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/simple-banner.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json b/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json new file mode 100644 index 00000000000..61be3f78c4c --- /dev/null +++ b/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-1", + "test": "1" + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-2", + "test": "1" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": "abc" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement-1", + "test": "1" + }, + "impIDs": ["test-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id-1", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/error-ext-bidder.json b/adapters/missena/missenatest/supplemental/error-ext-bidder.json new file mode 100644 index 00000000000..fdc08f4704b --- /dev/null +++ b/adapters/missena/missenatest/supplemental/error-ext-bidder.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://publisher.com/url" + }, + "user": { + "buyeruid": "1" + }, + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": "abc" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing missenaExt parameters", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/error-imp-ext.json b/adapters/missena/missenatest/supplemental/error-imp-ext.json new file mode 100644 index 00000000000..3905efa6bab --- /dev/null +++ b/adapters/missena/missenatest/supplemental/error-imp-ext.json @@ -0,0 +1,23 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://publisher.com/url" + }, + "user": { + "buyeruid": "1" + }, + "imp": [ + { + "id": "test-imp-id", + "ext": "error" + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt object", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-204.json b/adapters/missena/missenatest/supplemental/status-204.json new file mode 100644 index 00000000000..59070ab4ecb --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-204.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-400.json b/adapters/missena/missenatest/supplemental/status-400.json new file mode 100644 index 00000000000..23a153208e3 --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-400.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 400, + "body": "Bad request from publisher." + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.", + "comparison": "literal" + } + ] + } \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-not-200.json b/adapters/missena/missenatest/supplemental/status-not-200.json new file mode 100644 index 00000000000..8c913791fc3 --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-not-200.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/params_test.go b/adapters/missena/params_test.go new file mode 100644 index 00000000000..e76b80b694f --- /dev/null +++ b/adapters/missena/params_test.go @@ -0,0 +1,50 @@ +package missena + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderMissena, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderMissena, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"apiKey": "PA-123456"}`, + `{"apiKey": "PA-123456", "placement": "sticky"}`, + `{"apiKey": "PA-123456", "test": "native"}`, +} + +var invalidParams = []string{ + `{"apiKey": ""}`, + `{"apiKey": 42}`, + `{"placement": 111}`, + `{"placement": "sticky"}`, + `{"apiKey": "PA-123456", "placement": 111}`, + `{"test": "native"}`, + `{"apiKey": "PA-123456", "test": 111}`, +} diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go index c29be0baa81..aa88c4a0a3e 100644 --- a/adapters/openx/openx.go +++ b/adapters/openx/openx.go @@ -169,7 +169,7 @@ func preprocess(imp *openrtb2.Imp, reqExt *openxReqExt) error { if imp.Video != nil { videoCopy := *imp.Video - if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory != nil && *bidderExt.Prebid.IsRewardedInventory == 1 { + if imp.Rwdd == 1 { videoCopy.Ext = json.RawMessage(`{"rewarded":1}`) } else { videoCopy.Ext = nil diff --git a/adapters/openx/openxtest/exemplary/video-rewarded.json b/adapters/openx/openxtest/exemplary/video-rewarded.json index b16a92f23ac..5e853948a63 100644 --- a/adapters/openx/openxtest/exemplary/video-rewarded.json +++ b/adapters/openx/openxtest/exemplary/video-rewarded.json @@ -14,13 +14,11 @@ } }, "instl": 1, + "rwdd": 1, "ext": { "bidder": { "unit": "539439964", "delDomain": "se-demo-d.openx.net" - }, - "prebid": { - "is_rewarded_inventory": 1 } } } @@ -46,7 +44,8 @@ } }, "tagid": "539439964", - "instl": 1 + "instl": 1, + "rwdd": 1 } ], "ext": { diff --git a/adapters/oraki/oraki.go b/adapters/oraki/oraki.go new file mode 100644 index 00000000000..0e29aa9bd5e --- /dev/null +++ b/adapters/oraki/oraki.go @@ -0,0 +1,152 @@ +package oraki + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + OrakiBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var orakiExt openrtb_ext.ImpExtOraki + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, err) + continue + } + if err := json.Unmarshal(bidderExt.Bidder, &orakiExt); err != nil { + errs = append(errs, err) + continue + } + + impExt := reqBodyExt{OrakiBidderExt: reqBodyExtBidder{}} + + if orakiExt.PlacementID != "" { + impExt.OrakiBidderExt.PlacementID = orakiExt.PlacementID + impExt.OrakiBidderExt.Type = "publisher" + } else if orakiExt.EndpointID != "" { + impExt.OrakiBidderExt.EndpointID = orakiExt.EndpointID + impExt.OrakiBidderExt.Type = "network" + } + + finalyImpExt, err := json.Marshal(impExt) + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].Ext = finalyImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + errs = append(errs, err) + continue + } + + adapterRequests = append(adapterRequests, adapterReq) + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if len(response.Cur) != 0 { + bidResponse.Currency = response.Cur + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bid := seatBid.Bid[i] + bidType, err := getBidType(bid) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + // determinate media type by bid response field mtype + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + return "", fmt.Errorf("could not define media type for impression: %s", bid.ImpID) +} diff --git a/adapters/oraki/oraki_test.go b/adapters/oraki/oraki_test.go new file mode 100644 index 00000000000..f801e9816ed --- /dev/null +++ b/adapters/oraki/oraki_test.go @@ -0,0 +1,20 @@ +package oraki + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderOraki, config.Adapter{ + Endpoint: "https://fake.test.io/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "orakitest", bidder) +} diff --git a/adapters/oraki/orakitest/exemplary/endpointId.json b/adapters/oraki/orakitest/exemplary/endpointId.json new file mode 100644 index 00000000000..fb211d4e765 --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/endpointId.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/multi-format.json b/adapters/oraki/orakitest/exemplary/multi-format.json new file mode 100644 index 00000000000..cb1c6ffd9c6 --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/multi-format.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/multi-imp.json b/adapters/oraki/orakitest/exemplary/multi-imp.json new file mode 100644 index 00000000000..43bbf483960 --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/multi-imp.json @@ -0,0 +1,253 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id1", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + }, + { + "id": "test-imp-id2", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id1", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id1"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id1", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id2", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id2"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id2", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id1", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id2", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/simple-banner.json b/adapters/oraki/orakitest/exemplary/simple-banner.json new file mode 100644 index 00000000000..ee6352c92c9 --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/simple-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/simple-native.json b/adapters/oraki/orakitest/exemplary/simple-native.json new file mode 100644 index 00000000000..f517a686ccd --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/simple-native.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/simple-video.json b/adapters/oraki/orakitest/exemplary/simple-video.json new file mode 100644 index 00000000000..d51d8b59f75 --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/simple-video.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/exemplary/simple-web-banner.json b/adapters/oraki/orakitest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..7b890db4e9f --- /dev/null +++ b/adapters/oraki/orakitest/exemplary/simple-web-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/oraki/orakitest/supplemental/bad_media_type.json b/adapters/oraki/orakitest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..0708ed4ae31 --- /dev/null +++ b/adapters/oraki/orakitest/supplemental/bad_media_type.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": {} + } + ], + "seat": "oraki" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "could not define media type for impression: test-imp-id", + "comparison": "literal" + } + ] +} diff --git a/adapters/oraki/orakitest/supplemental/bad_response.json b/adapters/oraki/orakitest/supplemental/bad_response.json new file mode 100644 index 00000000000..d8029b5ed6b --- /dev/null +++ b/adapters/oraki/orakitest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/oraki/orakitest/supplemental/status-204.json b/adapters/oraki/orakitest/supplemental/status-204.json new file mode 100644 index 00000000000..f72a2eb0607 --- /dev/null +++ b/adapters/oraki/orakitest/supplemental/status-204.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/oraki/orakitest/supplemental/status-not-200.json b/adapters/oraki/orakitest/supplemental/status-not-200.json new file mode 100644 index 00000000000..218aa632618 --- /dev/null +++ b/adapters/oraki/orakitest/supplemental/status-not-200.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://fake.test.io/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/oraki/params_test.go b/adapters/oraki/params_test.go new file mode 100644 index 00000000000..15981de5fa7 --- /dev/null +++ b/adapters/oraki/params_test.go @@ -0,0 +1,47 @@ +package oraki + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderOraki, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderOraki, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/ownadx/ownadx.go b/adapters/ownadx/ownadx.go index 5678808b99b..7c858cd6d85 100644 --- a/adapters/ownadx/ownadx.go +++ b/adapters/ownadx/ownadx.go @@ -55,9 +55,9 @@ func createBidRequest(rtbBidRequest *openrtb2.BidRequest, imps []openrtb2.Imp) * } func (adapter *adapter) buildEndpointURL(params *openrtb_ext.ExtImpOwnAdx) (string, error) { endpointParams := macros.EndpointTemplateParams{ - ZoneID: params.SspId, - AccountID: params.SeatId, - SourceId: params.TokenId, + SspID: params.SspId, // Macro + SeatID: params.SeatId, + TokenID: params.TokenId, } return macros.ResolveMacros(adapter.endpoint, endpointParams) } @@ -123,6 +123,7 @@ func groupImpsByExt(imps []openrtb2.Imp) (map[openrtb_ext.ExtImpOwnAdx][]openrtb } func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { return nil, nil } @@ -158,6 +159,7 @@ func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR seatBid := bidResp.SeatBid[0] bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + if len(seatBid.Bid) == 0 { return nil, []error{ &errortypes.BadServerResponse{ @@ -168,7 +170,6 @@ func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR for i := 0; i < len(seatBid.Bid); i++ { var bidType openrtb_ext.BidType bid := seatBid.Bid[i] - bidType, err := getMediaType(bid) if err != nil { return nil, []error{&errortypes.BadServerResponse{ diff --git a/adapters/ownadx/ownadx_test.go b/adapters/ownadx/ownadx_test.go index 07dc928b9b0..5995cdd10a7 100644 --- a/adapters/ownadx/ownadx_test.go +++ b/adapters/ownadx/ownadx_test.go @@ -11,7 +11,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderOwnAdx, config.Adapter{ - Endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.SeatID}}/{{.SspID}}?token={{.TokenID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) assert.NoError(t, buildErr) adapterstest.RunJSONBidderTest(t, "ownadxtest", bidder) diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json index 9b1c03b91dd..7aa09d03ce0 100644 --- a/adapters/pubmatic/pubmatictest/exemplary/video.json +++ b/adapters/pubmatic/pubmatictest/exemplary/video.json @@ -176,7 +176,8 @@ }, "type": "video", "video" :{ - "duration" : 5 + "duration" : 5, + "primary_category": "" } } ] diff --git a/adapters/pubrise/params_test.go b/adapters/pubrise/params_test.go new file mode 100644 index 00000000000..df5d38fd02e --- /dev/null +++ b/adapters/pubrise/params_test.go @@ -0,0 +1,47 @@ +package pubrise + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderPubrise, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderPubrise, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/pubrise/pubrise.go b/adapters/pubrise/pubrise.go new file mode 100644 index 00000000000..9d71f2e1439 --- /dev/null +++ b/adapters/pubrise/pubrise.go @@ -0,0 +1,159 @@ +package pubrise + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + PubriseBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var pubriseExt openrtb_ext.ImpExtPubrise + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, err) + continue + } + if err := json.Unmarshal(bidderExt.Bidder, &pubriseExt); err != nil { + errs = append(errs, err) + continue + } + + impExt := reqBodyExt{PubriseBidderExt: reqBodyExtBidder{}} + + if pubriseExt.PlacementID != "" { + impExt.PubriseBidderExt.PlacementID = pubriseExt.PlacementID + impExt.PubriseBidderExt.Type = "publisher" + } else if pubriseExt.EndpointID != "" { + impExt.PubriseBidderExt.EndpointID = pubriseExt.EndpointID + impExt.PubriseBidderExt.Type = "network" + } + + finalyImpExt, err := json.Marshal(impExt) + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].Ext = finalyImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + errs = append(errs, err) + continue + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + + if len(adapterRequests) == 0 { + errs = append(errs, errors.New("found no valid impressions")) + return nil, errs + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + if len(response.Cur) != 0 { + bidResponse.Currency = response.Cur + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidType(seatBid.Bid[i]) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + // determinate media type by bid response field mtype + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + return "", fmt.Errorf("could not define media type for impression: %s", bid.ImpID) +} diff --git a/adapters/pubrise/pubrise_test.go b/adapters/pubrise/pubrise_test.go new file mode 100644 index 00000000000..a50878c339e --- /dev/null +++ b/adapters/pubrise/pubrise_test.go @@ -0,0 +1,20 @@ +package pubrise + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderEmtv, config.Adapter{ + Endpoint: "https://backend.pubrise.ai/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "pubrisetest", bidder) +} diff --git a/adapters/pubrise/pubrisetest/exemplary/endpointId.json b/adapters/pubrise/pubrisetest/exemplary/endpointId.json new file mode 100644 index 00000000000..3766c7a3ef4 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/endpointId.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/multi-format.json b/adapters/pubrise/pubrisetest/exemplary/multi-format.json new file mode 100644 index 00000000000..9e5c2ef2c57 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/multi-format.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/multi-imp.json b/adapters/pubrise/pubrisetest/exemplary/multi-imp.json new file mode 100644 index 00000000000..d922113a512 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/multi-imp.json @@ -0,0 +1,253 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + }, + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-banner.json b/adapters/pubrise/pubrisetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..6669c460ea1 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-native.json b/adapters/pubrise/pubrisetest/exemplary/simple-native.json new file mode 100644 index 00000000000..e535c9ce5f9 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-native.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-video.json b/adapters/pubrise/pubrisetest/exemplary/simple-video.json new file mode 100644 index 00000000000..047b2bc7d99 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-video.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json b/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..ef9b8080ae7 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json b/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..2b6165345ac --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": {} + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "could not define media type for impression: test-imp-id", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/bad_response.json b/adapters/pubrise/pubrisetest/supplemental/bad_response.json new file mode 100644 index 00000000000..08b58d888ed --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json b/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json new file mode 100644 index 00000000000..cc1edd685f9 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json @@ -0,0 +1,20 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "found no valid impressions", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/status-204.json b/adapters/pubrise/pubrisetest/supplemental/status-204.json new file mode 100644 index 00000000000..1ed98ff0c72 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/status-204.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/status-not-200.json b/adapters/pubrise/pubrisetest/supplemental/status-not-200.json new file mode 100644 index 00000000000..c4b3cdc6f57 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/status-not-200.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/rtbhouse/rtbhouse.go b/adapters/rtbhouse/rtbhouse.go index 2065163c660..47760188438 100644 --- a/adapters/rtbhouse/rtbhouse.go +++ b/adapters/rtbhouse/rtbhouse.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "github.com/buger/jsonparser" @@ -160,8 +161,9 @@ func (adapter *RTBHouseAdapter) MakeBids( var typedBid *adapters.TypedBid for _, seatBid := range openRTBBidderResponse.SeatBid { for _, bid := range seatBid.Bid { - bid := bid // pin! -> https://github.com/kyoh86/scopelint#whats-this + bid := bid bidType, err := getMediaTypeForBid(bid) + resolveMacros(&bid) if err != nil { errs = append(errs, err) continue @@ -221,3 +223,11 @@ func getNativeAdm(adm string) (string, error) { return adm, nil } + +func resolveMacros(bid *openrtb2.Bid) { + if bid != nil { + price := strconv.FormatFloat(bid.Price, 'f', -1, 64) + bid.NURL = strings.Replace(bid.NURL, "${AUCTION_PRICE}", price, -1) + bid.AdM = strings.Replace(bid.AdM, "${AUCTION_PRICE}", price, -1) + } +} diff --git a/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json b/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json new file mode 100644 index 00000000000..eaa3965040e --- /dev/null +++ b/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/prebid_server", + "body": { + "id": "test-request-id", + "cur": ["USD"], + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "seat": "rtbhouse", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype": 1 + }] + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 01b1e79799e..3ef76f58484 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -3,6 +3,7 @@ package rubicon import ( "encoding/json" "fmt" + "github.com/prebid/prebid-server/v2/version" "net/http" "net/url" "strconv" @@ -25,6 +26,7 @@ var bannerExtContent = []byte(`{"rp":{"mime":"text/html"}}`) type RubiconAdapter struct { URI string + externalURI string XAPIUsername string XAPIPassword string } @@ -219,6 +221,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co bidder := &RubiconAdapter{ URI: uri, + externalURI: server.ExternalUrl, XAPIUsername: config.XAPI.Username, XAPIPassword: config.XAPI.Password, } @@ -271,7 +274,7 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada rubiconRequest := *request for imp, bidderExt := range impsToExtMap { rubiconExt := bidderExt.Bidder - target, err := updateImpRpTargetWithFpdAttributes(bidderExt, rubiconExt, *imp, request.Site, request.App) + target, err := a.updateImpRpTarget(bidderExt, rubiconExt, *imp, request.Site, request.App) if err != nil { errs = append(errs, err) continue @@ -317,9 +320,11 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada continue } - if resolvedBidFloor > 0 { - imp.BidFloorCur = "USD" + if resolvedBidFloor >= 0 { imp.BidFloor = resolvedBidFloor + if imp.BidFloorCur != "" { + imp.BidFloorCur = "USD" + } } if request.User != nil { @@ -647,7 +652,7 @@ func resolveBidFloor(bidFloor float64, bidFloorCur string, reqInfo *adapters.Ext return bidFloor, nil } -func updateImpRpTargetWithFpdAttributes(extImp rubiconExtImpBidder, extImpRubicon openrtb_ext.ExtImpRubicon, +func (a *RubiconAdapter) updateImpRpTarget(extImp rubiconExtImpBidder, extImpRubicon openrtb_ext.ExtImpRubicon, imp openrtb2.Imp, site *openrtb2.Site, app *openrtb2.App) (json.RawMessage, error) { existingTarget, _, _, err := jsonparser.Get(imp.Ext, "rp", "target") @@ -740,6 +745,11 @@ func updateImpRpTargetWithFpdAttributes(extImp rubiconExtImpBidder, extImpRubico if len(extImpRubicon.Keywords) > 0 { addStringArrayAttribute(extImpRubicon.Keywords, target, "keywords") } + + target["pbs_login"] = a.XAPIUsername + target["pbs_version"] = version.Ver + target["pbs_url"] = a.externalURI + updatedTarget, err := json.Marshal(target) if err != nil { return nil, err diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index 7a420a10eca..ba72727c41c 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -232,6 +232,14 @@ func TestOpenRTBRequestWithDifferentBidFloorAttributes(t *testing.T) { expectedBidCur: "", expectedErrors: nil, }, + { + bidFloor: 0, + bidFloorCur: "EUR", + setMock: func(m *mock.Mock) {}, + expectedBidFloor: 0, + expectedBidCur: "USD", + expectedErrors: nil, + }, { bidFloor: -1, bidFloorCur: "CZK", @@ -600,6 +608,73 @@ func TestOpenRTBFirstPartyDataPopulating(t *testing.T) { } } +func TestPbsHostInfoPopulating(t *testing.T) { + bidder := RubiconAdapter{ + URI: "url", + externalURI: "externalUrl", + XAPIUsername: "username", + XAPIPassword: "password", + } + + request := &openrtb2.BidRequest{ + ID: "test-request-id", + Imp: []openrtb2.Imp{{ + ID: "test-imp-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + {W: 300, H: 250}, + }, + }, + Ext: json.RawMessage(`{ + "bidder": { + "zoneId": 8394, + "siteId": 283282, + "accountId": 7891, + "inventory": {"key1" : "val1"}, + "visitor": {"key2" : "val2"} + } + }`), + }}, + App: &openrtb2.App{ + ID: "com.test", + Name: "testApp", + }, + } + + reqs, _ := bidder.MakeRequests(request, &adapters.ExtraRequestInfo{}) + + rubiconReq := &openrtb2.BidRequest{} + if err := json.Unmarshal(reqs[0].Body, rubiconReq); err != nil { + t.Fatalf("Unexpected error while decoding request: %s", err) + } + + var rpImpExt rubiconImpExt + if err := json.Unmarshal(rubiconReq.Imp[0].Ext, &rpImpExt); err != nil { + t.Fatalf("Error unmarshalling imp.ext: %s", err) + } + + var pbsLogin string + pbsLogin, err := jsonparser.GetString(rpImpExt.RP.Target, "pbs_login") + if err != nil { + t.Fatal("Error extracting pbs_login") + } + assert.Equal(t, pbsLogin, "username", "Unexpected pbs_login value") + + var pbsVersion string + pbsVersion, err = jsonparser.GetString(rpImpExt.RP.Target, "pbs_version") + if err != nil { + t.Fatal("Error extracting pbs_version") + } + assert.Equal(t, pbsVersion, "", "Unexpected pbs_version value") + + var pbsUrl string + pbsUrl, err = jsonparser.GetString(rpImpExt.RP.Target, "pbs_url") + if err != nil { + t.Fatal("Error extracting pbs_url") + } + assert.Equal(t, pbsUrl, "externalUrl", "Unexpected pbs_url value") +} + func TestOpenRTBRequestWithBadvOverflowed(t *testing.T) { bidder := new(RubiconAdapter) @@ -982,7 +1057,7 @@ func TestOpenRTBResponseOverridePriceFromCorrespondingImp(t *testing.T) { "siteId": 68780, "zoneId": 327642, "debug": { - "cpmoverride" : 20 + "cpmoverride" : 20 } }}`), }}, diff --git a/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json b/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json index 581a0eb5308..fbbe72332dd 100644 --- a/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json +++ b/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json @@ -364,7 +364,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json b/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json index 279daec96c9..8bde0ff8004 100644 --- a/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json @@ -349,7 +349,10 @@ "sectioncat": [ "sectionCat1", "sectionCat2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json b/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json index 71043771d1d..0f3c97dc1e4 100644 --- a/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json +++ b/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json @@ -106,7 +106,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", @@ -212,7 +215,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/flexible-schema.json b/adapters/rubicon/rubicontest/exemplary/flexible-schema.json index 5af9224a1ea..f2fa5b58cbc 100644 --- a/adapters/rubicon/rubicontest/exemplary/flexible-schema.json +++ b/adapters/rubicon/rubicontest/exemplary/flexible-schema.json @@ -349,7 +349,10 @@ "sectioncat": [ "sectionCat1", "sectionCat2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json b/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json index dc3b9f64d2b..c1842cbe8fb 100644 --- a/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json +++ b/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json @@ -323,7 +323,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json b/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json index 73fc01a51e2..3e583e6741c 100644 --- a/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json +++ b/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json @@ -105,7 +105,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-banner.json b/adapters/rubicon/rubicontest/exemplary/simple-banner.json index 83073f0cb71..bfe9bb24926 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-banner.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-banner.json @@ -323,7 +323,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-native.json b/adapters/rubicon/rubicontest/exemplary/simple-native.json index 976d05c1d5d..a3bdeb49bff 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-native.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-native.json @@ -307,7 +307,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-video.json b/adapters/rubicon/rubicontest/exemplary/simple-video.json index 3546af36dbd..b022f516cf5 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-video.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-video.json @@ -322,7 +322,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json b/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json index ff9b6384905..a5b7fe9dd73 100644 --- a/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json @@ -473,7 +473,10 @@ "sectionCat1", "sectionCat2" ], - "dfp_ad_unit_code": "adSlotFromData" + "dfp_ad_unit_code": "adSlotFromData", + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/user-fpd.json b/adapters/rubicon/rubicontest/exemplary/user-fpd.json index 0c4e1a95592..97aaa1bf68a 100644 --- a/adapters/rubicon/rubicontest/exemplary/user-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/user-fpd.json @@ -275,7 +275,10 @@ "search": [ "someSearch" ], - "dfp_ad_unit_code": "someAdSlot" + "dfp_ad_unit_code": "someAdSlot", + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json index e10806cdc14..6556568ca36 100644 --- a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json +++ b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json @@ -234,7 +234,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content.json b/adapters/rubicon/rubicontest/supplemental/no-site-content.json index a86127b5822..aac2800c210 100644 --- a/adapters/rubicon/rubicontest/supplemental/no-site-content.json +++ b/adapters/rubicon/rubicontest/supplemental/no-site-content.json @@ -230,7 +230,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/smaato/smaatotest/supplemental/no-app-site-request.json b/adapters/smaato/smaatotest/supplemental/no-app-site-request.json deleted file mode 100644 index 04a73b4f40d..00000000000 --- a/adapters/smaato/smaatotest/supplemental/no-app-site-request.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "mockBidRequest": { - "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", - "imp": [ - { - "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", - "banner": { - "format": [ - { - "w": 320, - "h": 50 - } - ] - }, - "ext": { - "bidder": { - "publisherId": "1100042525", - "adspaceId": "130563103" - } - } - } - ] - }, - "expectedMakeRequestsErrors": [ - { - "value": "Missing Site/App.", - "comparison": "literal" - } - ] -} \ No newline at end of file diff --git a/adapters/sonobi/sonobi.go b/adapters/sonobi/sonobi.go index 385149f1517..07e301cd7a7 100644 --- a/adapters/sonobi/sonobi.go +++ b/adapters/sonobi/sonobi.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" @@ -53,6 +54,25 @@ func (a *SonobiAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adap reqCopy.Imp[0].TagID = sonobiExt.TagID + // If the bid floor currency is not USD, do the conversion to USD + if reqCopy.Imp[0].BidFloor > 0 && reqCopy.Imp[0].BidFloorCur != "" && strings.ToUpper(reqCopy.Imp[0].BidFloorCur) != "USD" { + + // Convert to US dollars + convertedValue, err := reqInfo.ConvertCurrency(reqCopy.Imp[0].BidFloor, reqCopy.Imp[0].BidFloorCur, "USD") + if err != nil { + errs = append(errs, err) + continue + } + + // Update after conversion. All imp elements inside request.Imp are shallow copies + // therefore, their non-pointer values are not shared memory and are safe to modify. + reqCopy.Imp[0].BidFloorCur = "USD" + reqCopy.Imp[0].BidFloor = convertedValue + } + + // Sonobi only bids in USD + reqCopy.Cur = append(make([]string, 0, 1), "USD") + adapterReq, errors := a.makeRequest(&reqCopy) if adapterReq != nil { adapterRequests = append(adapterRequests, adapterReq) @@ -114,19 +134,19 @@ func (a *SonobiAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR } bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + bidResponse.Currency = "USD" // Sonobi only bids in USD for _, sb := range bidResp.SeatBid { for i := range sb.Bid { - bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + bid := sb.Bid[i] + bidType, err := getMediaTypeForImp(bid.ImpID, internalRequest.Imp) if err != nil { - errs = append(errs, err) - } else { - b := &adapters.TypedBid{ - Bid: &sb.Bid[i], - BidType: bidType, - } - bidResponse.Bids = append(bidResponse.Bids, b) + return nil, []error{err} } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) } } return bidResponse, errs @@ -139,6 +159,9 @@ func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, if imp.Banner == nil && imp.Video != nil { mediaType = openrtb_ext.BidTypeVideo } + if imp.Banner == nil && imp.Video == nil && imp.Native != nil { + mediaType = openrtb_ext.BidTypeNative + } return mediaType, nil } } diff --git a/adapters/sonobi/sonobitest/exemplary/banner.json b/adapters/sonobi/sonobitest/exemplary/banner.json index 06a7a6724a7..8e7ddd49545 100644 --- a/adapters/sonobi/sonobitest/exemplary/banner.json +++ b/adapters/sonobi/sonobitest/exemplary/banner.json @@ -1,5 +1,6 @@ { "mockBidRequest": { + "cur": ["GBP"], "id": "some-request-id", "site": { "page": "http://tester.go.sonobi.com", @@ -48,6 +49,7 @@ }, "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", "body": { + "cur": ["USD"], "id": "some-request-id", "imp": [ { diff --git a/adapters/sonobi/sonobitest/exemplary/native.json b/adapters/sonobi/sonobitest/exemplary/native.json new file mode 100644 index 00000000000..ff9eb4c4693 --- /dev/null +++ b/adapters/sonobi/sonobitest/exemplary/native.json @@ -0,0 +1,143 @@ +{ + "mockBidRequest": { + "cur": ["USD"], + "id": "some-request-id", + "site": { + "page": "http://tester.go.sonobi.com", + "domain": "sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":2,\"plcmtcnt\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":1000}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":600,\"hmin\":600}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":3000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":60}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}},{\"id\":10,\"required\":0,\"data\":{\"type\":12,\"len\":15}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"privacy\":1}", + "ver": "1.2", + "battr": [ + 1, + 2, + 6, + 7, + 8, + 9, + 10, + 14 + ] + }, + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", + "body": { + "cur": ["USD"], + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":2,\"plcmtcnt\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":1000}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":600,\"hmin\":600}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":3000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":60}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}},{\"id\":10,\"required\":0,\"data\":{\"type\":12,\"len\":15}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"privacy\":1}", + "ver": "1.2", + "battr": [ + 1, + 2, + 6, + 7, + 8, + 9, + 10, + 14 + ] + }, + "tagid": "/7780971/apex_3pdm_integration", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "site": { + "domain": "sonobi.com", + "page": "http://tester.go.sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "test-markup", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox" + + } + ], + "seat": "sonobi" + } + ], + "bidid": "sandbox_642305097", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "test-markup", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox" + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/sonobi/sonobitest/exemplary/no-bid.json b/adapters/sonobi/sonobitest/exemplary/no-bid.json index e931dbdcd9e..5f3c2afb1dd 100644 --- a/adapters/sonobi/sonobitest/exemplary/no-bid.json +++ b/adapters/sonobi/sonobitest/exemplary/no-bid.json @@ -1,5 +1,6 @@ { "mockBidRequest": { + "cur": ["USD"], "id": "some-request-id", "site": { "page": "http://tester.go.sonobi.com", @@ -48,6 +49,7 @@ }, "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", "body": { + "cur": ["USD"], "id": "some-request-id", "imp": [ { diff --git a/adapters/sonobi/sonobitest/supplemental/currency-conversion.json b/adapters/sonobi/sonobitest/supplemental/currency-conversion.json new file mode 100644 index 00000000000..522e1bd7326 --- /dev/null +++ b/adapters/sonobi/sonobitest/supplemental/currency-conversion.json @@ -0,0 +1,172 @@ +{ + "mockBidRequest": { + "cur": ["GBP"], + "id": "some-request-id", + "site": { + "page": "http://tester.go.sonobi.com", + "domain": "sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 1.00, + "bidfloorcur": "GBP", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "GBP": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + }, + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", + "body": { + "cur": ["USD"], + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "bidfloor": 0.05, + "bidfloorcur": "USD", + "tagid": "/7780971/apex_3pdm_integration", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "GBP": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + }, + "site": { + "domain": "sonobi.com", + "page": "http://tester.go.sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox", + "h": 1, + "w": 1 + } + ], + "seat": "sonobi" + } + ], + "bidid": "sandbox_642305097", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox", + "h": 1, + "w": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/taboola/taboola.go b/adapters/taboola/taboola.go index 85a129eb341..01caafed610 100644 --- a/adapters/taboola/taboola.go +++ b/adapters/taboola/taboola.go @@ -131,7 +131,14 @@ func (a *adapter) buildRequest(request *openrtb2.BidRequest) (*adapters.RequestD return nil, fmt.Errorf("unsupported media type for imp: %v", request.Imp[0]) } - url, err := a.buildEndpointURL(request.Site.ID, mediaType) + var taboolaPublisherId string + if request.Site != nil && request.Site.ID != "" { + taboolaPublisherId = request.Site.ID + } else if request.App != nil && request.App.ID != "" { + taboolaPublisherId = request.App.ID + } + + url, err := a.buildEndpointURL(taboolaPublisherId, mediaType) if err != nil { return nil, err } @@ -206,22 +213,20 @@ func createTaboolaRequests(request *openrtb2.BidRequest) (taboolaRequests []*ope ID: taboolaExt.PublisherId, } - if modifiedRequest.Site == nil { - newSite := &openrtb2.Site{ - ID: taboolaExt.PublisherId, - Name: taboolaExt.PublisherId, - Domain: evaluateDomain(taboolaExt.PublisherDomain, request), - Publisher: publisher, - } - modifiedRequest.Site = newSite - } else { + if modifiedRequest.Site != nil { modifiedSite := *modifiedRequest.Site - modifiedSite.Publisher = publisher modifiedSite.ID = taboolaExt.PublisherId modifiedSite.Name = taboolaExt.PublisherId modifiedSite.Domain = evaluateDomain(taboolaExt.PublisherDomain, request) + modifiedSite.Publisher = publisher modifiedRequest.Site = &modifiedSite } + if modifiedRequest.App != nil { + modifiedApp := *modifiedRequest.App + modifiedApp.ID = taboolaExt.PublisherId + modifiedApp.Publisher = publisher + modifiedRequest.App = &modifiedApp + } if taboolaExt.BCat != nil { modifiedRequest.BCat = taboolaExt.BCat diff --git a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json b/adapters/taboola/taboolatest/exemplary/bannerAppRequest.json similarity index 92% rename from adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json rename to adapters/taboola/taboolatest/exemplary/bannerAppRequest.json index 6e01c457aea..60cec3ca1d5 100644 --- a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json +++ b/adapters/taboola/taboolatest/exemplary/bannerAppRequest.json @@ -23,13 +23,14 @@ "ext": { "bidder": { "publisherId": "publisher-id", - "tagid": "tag-id" + "tagid": "tag-id", + "tagId": "tag-Id" } } } ], "app": { - "domain": "http://domain.com" + "bundle": "com.app.my" }, "device": { "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36", @@ -62,21 +63,19 @@ } ] }, - "tagid" : "tag-id", + "tagid" : "tag-Id", "ext": { "bidder": { "publisherId": "publisher-id", - "tagid": "tag-id" + "tagid": "tag-id", + "tagId": "tag-Id" } } } ], "app": { - "domain": "http://domain.com" - }, - "site": { "id": "publisher-id", - "name": "publisher-id", + "bundle": "com.app.my", "publisher": { "id": "publisher-id" } diff --git a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json index b040d31b5f6..5538150f450 100644 --- a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json +++ b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json @@ -20,7 +20,6 @@ } ] }, - "httpCalls": [ { "expectedRequest": { @@ -29,7 +28,7 @@ "id": "test-request-id", "imp": [ { - "id":"test-imp-id", + "id": "test-imp-id", "video": { "w": 300, "h": 250, @@ -45,7 +44,10 @@ } } ] - } + }, + "impIDs": [ + "test-imp-id" + ] }, "mockResponse": { "status": 200, @@ -54,13 +56,20 @@ "seatbid": [ { "seat": "ttx", - "bid": [{ - "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", - "impid": "test-imp-id", - "price": 1.2, - "adm": "some-ads", - "crid": "crid_testid" - }] + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some-ads", + "crid": "crid_testid", + "ext": { + "video": { + "duration": 300 + } + } + } + ] } ], "cur": "USD" @@ -68,7 +77,6 @@ } } ], - "expectedBidResponses": [ { "currency": "USD", @@ -79,9 +87,18 @@ "impid": "test-imp-id", "price": 1.2, "adm": "some-ads", - "crid": "crid_testid" + "crid": "crid_testid", + "ext": { + "video": { + "duration": 300 + } + } }, - "type": "video" + "type": "video", + "video": { + "duration": 300, + "primary_category": "" + } } ] } diff --git a/amp/parse.go b/amp/parse.go index 12663ee93bd..a39888eee9f 100644 --- a/amp/parse.go +++ b/amp/parse.go @@ -117,7 +117,7 @@ func buildGdprTCF2ConsentWriter(ampParams Params) gdpr.ConsentWriter { // set regs.ext.gdpr if non-nil gdpr_applies was set to true gdprValue = parseGdprApplies(ampParams.GdprApplies) } - writer.RegExtGDPR = &gdprValue + writer.GDPR = &gdprValue return writer } diff --git a/amp/parse_test.go b/amp/parse_test.go index f2f097284c5..98006325e72 100644 --- a/amp/parse_test.go +++ b/amp/parse_test.go @@ -311,8 +311,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - RegExtGDPR: &int8One, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + GDPR: &int8One, }, warning: nil, }, @@ -378,8 +378,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - RegExtGDPR: &int8One, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + GDPR: &int8One, }, warning: nil, }, @@ -400,8 +400,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "INVALID_GDPR", - RegExtGDPR: &int8One, + Consent: "INVALID_GDPR", + GDPR: &int8One, }, warning: &errortypes.Warning{ Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", @@ -420,8 +420,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "INVALID_GDPR", - RegExtGDPR: &int8Zero, + Consent: "INVALID_GDPR", + GDPR: &int8Zero, }, warning: &errortypes.Warning{ Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", @@ -440,8 +440,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - RegExtGDPR: &int8Zero, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + GDPR: &int8Zero, }, warning: nil, }, @@ -457,8 +457,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - RegExtGDPR: &int8One, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + GDPR: &int8One, }, warning: nil, }, @@ -473,8 +473,8 @@ func TestPrivacyReader(t *testing.T) { }, expected: expectedResults{ policyWriter: gdpr.ConsentWriter{ - Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - RegExtGDPR: &int8One, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + GDPR: &int8One, }, warning: nil, }, @@ -559,8 +559,8 @@ func TestBuildGdprTCF2ConsentWriter(t *testing.T) { desc: "gdpr_applies not set", inParams: Params{Consent: consentString}, expectedWriter: gdpr.ConsentWriter{ - Consent: consentString, - RegExtGDPR: &int8One, + Consent: consentString, + GDPR: &int8One, }, }, { @@ -570,8 +570,8 @@ func TestBuildGdprTCF2ConsentWriter(t *testing.T) { GdprApplies: &boolFalse, }, expectedWriter: gdpr.ConsentWriter{ - Consent: consentString, - RegExtGDPR: &int8Zero, + Consent: consentString, + GDPR: &int8Zero, }, }, { @@ -581,8 +581,8 @@ func TestBuildGdprTCF2ConsentWriter(t *testing.T) { GdprApplies: &boolTrue, }, expectedWriter: gdpr.ConsentWriter{ - Consent: consentString, - RegExtGDPR: &int8One, + Consent: consentString, + GDPR: &int8One, }, }, } diff --git a/analytics/agma/README.md b/analytics/agma/README.md new file mode 100644 index 00000000000..430001863ac --- /dev/null +++ b/analytics/agma/README.md @@ -0,0 +1,28 @@ +# agma Analytics + +In order to use the Agma Analytics Adapter, please adjust the accounts / endpoint with the data provided by agma (https://www.agma-mmc.de). + +## Configuration + +```yaml +analytics: + agma: + # Required: enable the module + enabled: true + # Required: set the accounts you want to track + accounts: + - code: "my-code" # Required: provied by agma + publisher_id: "123" # Required: Exchange specific publisher_id, can be an empty string accounts are not used + site_app_id: "openrtb2-site.id-or-app.id-or-app.bundle" # optional: scope to the publisher with an openrtb2 Site object id or App object id/bundle + # Optional properties (advanced configuration) + endpoint: + url: "https://go.pbs.agma-analytics.de/v1/prebid-server" # Check with agma if your site needs an extra url + timeout: "2s" + gzip: true + buffers: # Flush events when (first condition reached) + # Size of the buffer in bytes + size: "2MB" # greater than 2MB (size using SI standard eg. "44kB", "17MB") + count : 100 # greater than 100 events + timeout: "15m" # greater than 15 minutes (parsed as golang duration) + +``` diff --git a/analytics/agma/agma_module.go b/analytics/agma/agma_module.go new file mode 100644 index 00000000000..58133db2608 --- /dev/null +++ b/analytics/agma/agma_module.go @@ -0,0 +1,271 @@ +package agma + +import ( + "bytes" + "errors" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/benbjohnson/clock" + "github.com/docker/go-units" + "github.com/golang/glog" + "github.com/prebid/go-gdpr/vendorconsent" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type httpSender = func(payload []byte) error + +const ( + agmaGVLID = 1122 + p9 = 9 +) + +type AgmaLogger struct { + sender httpSender + clock clock.Clock + accounts []config.AgmaAnalyticsAccount + eventCount int64 + maxEventCount int64 + maxBufferByteSize int64 + maxDuration time.Duration + mux sync.RWMutex + sigTermCh chan os.Signal + buffer bytes.Buffer + bufferCh chan []byte +} + +func newAgmaLogger(cfg config.AgmaAnalytics, sender httpSender, clock clock.Clock) (*AgmaLogger, error) { + pSize, err := units.FromHumanSize(cfg.Buffers.BufferSize) + if err != nil { + return nil, err + } + pDuration, err := time.ParseDuration(cfg.Buffers.Timeout) + if err != nil { + return nil, err + } + if len(cfg.Accounts) == 0 { + return nil, errors.New("Please configure at least one account for Agma Analytics") + } + + buffer := bytes.Buffer{} + buffer.Write([]byte("[")) + + return &AgmaLogger{ + sender: sender, + clock: clock, + accounts: cfg.Accounts, + maxBufferByteSize: pSize, + eventCount: 0, + maxEventCount: int64(cfg.Buffers.EventCount), + maxDuration: pDuration, + buffer: buffer, + bufferCh: make(chan []byte), + sigTermCh: make(chan os.Signal, 1), + }, nil +} + +func NewModule(httpClient *http.Client, cfg config.AgmaAnalytics, clock clock.Clock) (analytics.Module, error) { + sender, err := createHttpSender(httpClient, cfg.Endpoint) + if err != nil { + return nil, err + } + + m, err := newAgmaLogger(cfg, sender, clock) + if err != nil { + return nil, err + } + + signal.Notify(m.sigTermCh, os.Interrupt, syscall.SIGTERM) + + go m.start() + + return m, nil +} + +func (l *AgmaLogger) start() { + ticker := l.clock.Ticker(l.maxDuration) + for { + select { + case <-l.sigTermCh: + glog.Infof("[AgmaAnalytics] Received Close, trying to flush buffer") + l.flush() + return + case event := <-l.bufferCh: + l.bufferEvent(event) + if l.isFull() { + l.flush() + } + case <-ticker.C: + l.flush() + } + } +} + +func (l *AgmaLogger) bufferEvent(data []byte) { + l.mux.Lock() + defer l.mux.Unlock() + + l.buffer.Write(data) + l.buffer.WriteByte(',') + l.eventCount++ +} + +func (l *AgmaLogger) isFull() bool { + l.mux.RLock() + defer l.mux.RUnlock() + return l.eventCount >= l.maxEventCount || int64(l.buffer.Len()) >= l.maxBufferByteSize +} + +func (l *AgmaLogger) flush() { + l.mux.Lock() + + if l.eventCount == 0 || l.buffer.Len() == 0 { + l.mux.Unlock() + return + } + + // Close the json array, remove last , + l.buffer.Truncate(l.buffer.Len() - 1) + l.buffer.Write([]byte("]")) + + payload := make([]byte, l.buffer.Len()) + _, err := l.buffer.Read(payload) + if err != nil { + l.reset() + l.mux.Unlock() + glog.Warning("[AgmaAnalytics] fail to copy the buffer") + return + } + + go l.sender(payload) + + l.reset() + l.mux.Unlock() +} + +func (l *AgmaLogger) reset() { + l.buffer.Reset() + l.buffer.Write([]byte("[")) + l.eventCount = 0 +} + +func (l *AgmaLogger) extractPublisherAndSite(requestWrapper *openrtb_ext.RequestWrapper) (string, string) { + publisherId := "" + appSiteId := "" + if requestWrapper.Site != nil { + if requestWrapper.Site.Publisher != nil { + publisherId = requestWrapper.Site.Publisher.ID + } + appSiteId = requestWrapper.Site.ID + } + if requestWrapper.App != nil { + if requestWrapper.App.Publisher != nil { + publisherId = requestWrapper.App.Publisher.ID + } + appSiteId = requestWrapper.App.ID + if appSiteId == "" { + appSiteId = requestWrapper.App.Bundle + } + + } + return publisherId, appSiteId +} + +func (l *AgmaLogger) shouldTrackEvent(requestWrapper *openrtb_ext.RequestWrapper) (bool, string) { + if requestWrapper.User == nil { + return false, "" + } + consentStr := requestWrapper.User.Consent + + parsedConsent, err := vendorconsent.ParseString(consentStr) + if err != nil { + return false, "" + } + + p9Allowed := parsedConsent.PurposeAllowed(p9) + agmaAllowed := parsedConsent.VendorConsent(agmaGVLID) + if !p9Allowed || !agmaAllowed { + return false, "" + } + + publisherId, appSiteId := l.extractPublisherAndSite(requestWrapper) + if publisherId == "" && appSiteId == "" { + return false, "" + } + + for _, account := range l.accounts { + if account.PublisherId == publisherId { + if account.SiteAppId == "" { + return true, account.Code + } + if account.SiteAppId == appSiteId { + return true, account.Code + } + } + } + + return false, "" +} + +func (l *AgmaLogger) LogAuctionObject(event *analytics.AuctionObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAuction, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing auction object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) LogAmpObject(event *analytics.AmpObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAmp, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing amp object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) LogVideoObject(event *analytics.VideoObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeVideo, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing video object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) Shutdown() { + glog.Info("[AgmaAnalytics] Shutdown, trying to flush buffer") + l.flush() // mutex safe +} + +func (l *AgmaLogger) LogCookieSyncObject(event *analytics.CookieSyncObject) {} +func (l *AgmaLogger) LogNotificationEventObject(event *analytics.NotificationEvent) {} +func (l *AgmaLogger) LogSetUIDObject(event *analytics.SetUIDObject) {} diff --git a/analytics/agma/agma_module_test.go b/analytics/agma/agma_module_test.go new file mode 100644 index 00000000000..213af1860be --- /dev/null +++ b/analytics/agma/agma_module_test.go @@ -0,0 +1,735 @@ +package agma + +import ( + "io" + "net/http" + "net/http/httptest" + "sync" + "syscall" + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var agmaConsent = "CP6-v9RP6-v9RNlAAAENCZCAAICAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A" + +var mockValidAuctionObject = analytics.AuctionObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }, +} + +var mockValidVideoObject = analytics.VideoObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "track-me-app", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }, +} + +var mockValidAmpObject = analytics.AmpObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }, +} + +var mockValidAccounts = []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "track-me-app", + }, + { + PublisherId: "track-me", + Code: "abcd", + SiteAppId: "track-me-site", + }, +} + +type MockedSender struct { + mock.Mock +} + +func (m *MockedSender) Send(payload []byte) error { + args := m.Called(payload) + return args.Error(0) +} + +func TestConfigParsingError(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + shouldFail bool + }{ + { + name: "Test with invalid/empty URL", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "%%2815197306101420000%29", + Timeout: "1s", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with invalid timout", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1x", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with no accounts", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1s", + Gzip: false, + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{}, + }, + shouldFail: true, + }, + } + clockMock := clock.NewMock() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewModule(&http.Client{}, tc.config, clockMock) + if tc.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestShouldTrackEvent(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + { + PublisherId: "", + SiteAppId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + // no userExt + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-not", + }, + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // no userExt + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No agma + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Consent: "CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIwgAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A", + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No Purpose 9 + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Consent: "CP4LywcP4LywcLRAAAENCZCAAIAAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A", + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // No valid sites / apps / empty publisher app + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "", + Publisher: &openrtb2.Publisher{ + ID: "", + }, + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // should allow empty accounts + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "track-me", + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "abc", code) + + // Bundle ID instead of app.id + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "track-me", + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "abc", code) +} + +func TestShouldTrackMultipleAccounts(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me-a", + Code: "abc", + }, + { + PublisherId: "track-me-b", + Code: "123", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-a", + }, + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "abc", code) + + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "site-test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-b", + }, + }, + User: &openrtb2.User{ + Consent: agmaConsent, + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "123", code) +} + +func TestShouldNotTrackLog(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + }{ + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "do-not-track", + Code: "abc", + }, + }, + }, + }, + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "do-not-track", + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(tc.config, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + clockMock.Add(2 * time.Minute) + mockedSender.AssertNumberOfCalls(t, "Send", 0) + assert.Zero(t, logger.eventCount) + }) + } +} + +func TestRaceAllEvents(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + clockMock.Add(10 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(3), logger.eventCount) + logger.mux.RUnlock() +} + +func TestFlushOnSigterm(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + done := make(chan struct{}) + go func() { + logger.start() + close(done) + }() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + logger.sigTermCh <- syscall.SIGTERM + <-done + + time.Sleep(100 * time.Millisecond) + + mockedSender.AssertCalled(t, "Send", mock.Anything) +} + +func TestRaceBufferCount(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + // Test EventCount Buffer + logger.LogAuctionObject(&mockValidAuctionObject) + + clockMock.Add(1 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(1), logger.eventCount) + logger.mux.RUnlock() + + assert.Equal(t, false, logger.isFull()) + + // add 1 more + logger.LogAuctionObject(&mockValidAuctionObject) + clockMock.Add(1 * time.Millisecond) + + // should trigger send and flash the buffer + mockedSender.AssertCalled(t, "Send", mock.Anything) + + logger.mux.RLock() + assert.Equal(t, int64(0), logger.eventCount) + logger.mux.RUnlock() +} + +func TestBufferSize(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "20Kb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 50; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Millisecond) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestBufferTime(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 5; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Minute) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestRaceEnd2End(t *testing.T) { + var mu sync.Mutex + + requestBodyAsString := "" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check for reponse + requestBody, err := io.ReadAll(r.Body) + mu.Lock() + requestBodyAsString = string(requestBody) + mu.Unlock() + if err != nil { + http.Error(w, "Error reading request body", 500) + return + } + + w.WriteHeader(http.StatusOK) + })) + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: server.URL, + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + + clockMock := clock.NewMock() + clockMock.Set(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)) + + logger, err := NewModule(&http.Client{}, cfg, clockMock) + assert.NoError(t, err) + + logger.LogAmpObject(&mockValidAmpObject) + logger.LogAmpObject(&mockValidAmpObject) + + time.Sleep(250 * time.Millisecond) + + expected := "[{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"consent\":\"" + agmaConsent + "\"},\"created_at\":\"2023-02-01T00:00:00Z\"},{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"consent\":\"" + agmaConsent + "\"},\"created_at\":\"2023-02-01T00:00:00Z\"}]" + + mu.Lock() + actual := requestBodyAsString + mu.Unlock() + + assert.Equal(t, expected, actual) +} + +func TestShutdownFlush(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + logger.LogAuctionObject(&mockValidAuctionObject) + logger.Shutdown() + + time.Sleep(10 * time.Millisecond) + + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go index 911de4c6959..6b16d0b7e92 100644 --- a/analytics/pubstack/pubstack_module_test.go +++ b/analytics/pubstack/pubstack_module_test.go @@ -99,7 +99,7 @@ func TestNewModuleSuccess(t *testing.T) { { ImpId: "123", StatusCode: 34, - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + Ext: &openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, }, }, }, diff --git a/config/config.go b/config/config.go index 05d6ca73fc8..aa132c4482e 100644 --- a/config/config.go +++ b/config/config.go @@ -65,9 +65,9 @@ type Configuration struct { VideoStoredRequestRequired bool `mapstructure:"video_stored_request_required"` - // Array of blacklisted apps that is used to create the hash table BlacklistedAppMap so App.ID's can be instantly accessed. - BlacklistedApps []string `mapstructure:"blacklisted_apps,flow"` - BlacklistedAppMap map[string]bool + // Array of blocked apps that is used to create the hash table BlockedAppsLookup so App.ID's can be instantly accessed. + BlockedApps []string `mapstructure:"blocked_apps,flow"` + BlockedAppsLookup map[string]bool // Is publisher/account ID required to be submitted in the OpenRTB2 request AccountRequired bool `mapstructure:"account_required"` // AccountDefaults defines default settings for valid accounts that are partially defined @@ -754,10 +754,10 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin } // To look for a request's app_id in O(1) time, we fill this hash table located in the - // the BlacklistedApps field of the Configuration struct defined in this file - c.BlacklistedAppMap = make(map[string]bool) - for i := 0; i < len(c.BlacklistedApps); i++ { - c.BlacklistedAppMap[c.BlacklistedApps[i]] = true + // the BlockedApps field of the Configuration struct defined in this file + c.BlockedAppsLookup = make(map[string]bool) + for i := 0; i < len(c.BlockedApps); i++ { + c.BlockedAppsLookup[c.BlockedApps[i]] = true } // Migrate combo stored request config to separate stored_reqs and amp stored_reqs configs. @@ -1087,8 +1087,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("default_request.type", "") v.SetDefault("default_request.file.name", "") v.SetDefault("default_request.alias_info", false) - v.SetDefault("blacklisted_apps", []string{""}) - v.SetDefault("blacklisted_accts", []string{""}) + v.SetDefault("blocked_apps", []string{""}) v.SetDefault("account_required", false) v.SetDefault("account_defaults.disabled", false) v.SetDefault("account_defaults.debug_allow", true) diff --git a/config/config_test.go b/config/config_test.go index a551c1be66e..df0e9c8ce0d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -435,7 +435,7 @@ metrics: adapter_connections_metrics: true adapter_gdpr_request_blocked: true account_modules_metrics: true -blacklisted_apps: ["spamAppID","sketchy-app-id"] +blocked_apps: ["spamAppID","sketchy-app-id"] account_required: true auto_gen_source_tid: false certificates_file: /etc/ssl/cert.pem @@ -638,12 +638,12 @@ func TestFullConfig(t *testing.T) { cmpBools(t, "lmt.enforce", true, cfg.LMT.Enforce) //Assert the NonStandardPublishers was correctly unmarshalled - cmpStrings(t, "blacklisted_apps", "spamAppID", cfg.BlacklistedApps[0]) - cmpStrings(t, "blacklisted_apps", "sketchy-app-id", cfg.BlacklistedApps[1]) + cmpStrings(t, "blocked_apps", "spamAppID", cfg.BlockedApps[0]) + cmpStrings(t, "blocked_apps", "sketchy-app-id", cfg.BlockedApps[1]) - //Assert the BlacklistedAppMap hash table was built correctly - for i := 0; i < len(cfg.BlacklistedApps); i++ { - cmpBools(t, "cfg.BlacklistedAppMap", true, cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]]) + //Assert the BlockedAppsLookup hash table was built correctly + for i := 0; i < len(cfg.BlockedApps); i++ { + cmpBools(t, "cfg.BlockedAppsLookup", true, cfg.BlockedAppsLookup[cfg.BlockedApps[i]]) } //Assert purpose VendorExceptionMap hash tables were built correctly diff --git a/docs/build/README.md b/docs/build/README.md new file mode 100644 index 00000000000..0b52671f216 --- /dev/null +++ b/docs/build/README.md @@ -0,0 +1,110 @@ +## Overview + +As of v2.31.0, Prebid Server contains a module that requires CGo which introduces both build and runtime dependencies. To build, you need a C compiler, preferably gcc. To run, you may require one or more runtime dependencies, most notably libatomic. + +## Examples +For a containerized example, see the Dockerfile. +For manual build examples, including some cross-compilation use cases, see below. + +### From darwin amd64 + +#### To darwin amd64 +`GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build` + +Running the built binary on mac amd64: +`./prebid-server --stderrthreshold=WARNING -v=2` + +#### To darwin arm64 +`GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build` + +Running the built binary on mac arm64: +`./prebid-server --stderrthreshold=WARNING -v=2` + +#### To windows amd64 +Build +Install mingw-w64 which consists of a gcc compiler port you can use to generate windows binaries: +`brew install mingw-w64` + +`GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC="x86_64-w64-mingw32-gcc" go build` + +Run +Running the built binary on windows: +`.\prebid-server.exe --sderrthreshold=WARNING =v=2` + +You may receive the following errors or something similar: +``` +"The code execution cannot proceed because libatomic-1.dll was not found." +"The code execution cannot proceed because libwinpthread-1.dll was not found." +``` + +To resolve these errors, copy the following files from mingw-64 on your mac to `C:/windows/System32` and re-run: +`/usr/local/Cellar/mingw-w64/12.0.0_1/toolchain-x86_64/x86_64-w64-mingw32/lib/libatomic-1.dll` +`/usr/local/Cellar/mingw-w64/12.0.0_1/toolchain-x86_64/x86_64-w64-mingw32/bin/libwinpthread-1.dll` + +### From windows amd64 +#### To windows amd64 +Build +`set CGO_ENABLED=1` +`set GOOS=windows` +`set GOARCH=amd64` +`go build . && .\prebid-server.exe --stderrthreshold=WARNING -v=2` + +You may receive the following error or something similar: +``` +# runtime/cgo +cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH% +``` + +To resolve the error, install MSYS2: +1) Download the installer (https://www.msys2.org/) +2) Run the installer and follow the steps of the installation wizard +3) Run MSYS2 which will open an MSYS2 terminal for you +4) In the MSYS2 terminal, install windows/amd64 gcc toolchain: `pacman -S --needed base-devel mingw-w64-x86_64-gcc` +5) Enter `Y` when prompted whether to proceed with the installation +6) Add the path of your MinGW-w64 `bin` folder to the Windows `PATH` environment variable by using the following steps: + - In the Windows search bar, type Settings to open your Windows Settings. + - Search for Edit environment variables for your account. + - In your User variables, select the `Path` variable and then select Edit. + - Select New and add the MinGW-w64 destination folder you recorded during the installation process to the list. If you used the default settings above, then this will be the path: `C:\msys64\ucrt64\bin`. + - Select OK, and then select OK again in the Environment Variables window to update the `PATH` environment variable. You have to reopen any console windows for the updated `PATH` environment variable to be available. +7) Confirm gcc installed: `gcc --version` + +Run +Running the built binary on windows: +`go build . && .\prebid-server.exe --stderrthreshold=WARNING -v=2` + +You may receive the following errors or something similar: +``` +"The code execution cannot proceed because libatomic-1.dll was not found." +"The code execution cannot proceed because libwinpthread-1.dll was not found." +``` +To resolve these errors, copy the following files from MSYS2 installation to `C:/windows/System32` and re-run: +`C:\mysys64\mingw64\bin\libatomic-1.dll` +`C:\mysys64\mingw64\bin\libwinpthread-1.dll` + +### From linux amd64 +#### To linux amd64 +Note +These instructions are for building and running on Debian-based distributions + +Build +`GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build` + +You may receive the following error or something similar: +``` +# runtime/cgo +cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH +``` +To resolve the error, install gcc and re-build: +`sudo apt-get install -y gcc` + +Run +Running the built binary on Linux: +`./prebid-server --stderrthreshold=WARNING -v=2` + +You may receive the following error or something similar: +``` +... error while loading shared libraries: libatomic.so.1: cannot open shared object file ... +``` +To resolve the error, install libatomic1 and re-run: +`sudo apt-get install -y libatomic1` \ No newline at end of file diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 84d6d7847ef..b691b2c0084 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "strconv" "strings" @@ -167,6 +168,11 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma tcf2Cfg := c.privacyConfig.tcf2ConfigBuilder(c.privacyConfig.gdprConfig.TCF2, account.GDPR) gdprPerms := c.privacyConfig.gdprPermissionsBuilder(tcf2Cfg, gdprRequestInfo) + limit := math.MaxInt + if request.Limit != nil { + limit = *request.Limit + } + rx := usersync.Request{ Bidders: request.Bidders, Cooperative: usersync.Cooperative{ @@ -174,7 +180,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma PriorityGroups: c.config.UserSync.PriorityGroups, }, Debug: request.Debug, - Limit: request.Limit, + Limit: limit, Privacy: usersyncPrivacy{ gdprPermissions: gdprPerms, ccpaParsedPolicy: ccpaParsedPolicy, @@ -278,17 +284,38 @@ func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) { } func (c *cookieSyncEndpoint) setLimit(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest { - if request.Limit <= 0 && cookieSyncConfig.DefaultLimit != nil { - request.Limit = *cookieSyncConfig.DefaultLimit + limit := getEffectiveLimit(request.Limit, cookieSyncConfig.DefaultLimit) + maxLimit := getEffectiveMaxLimit(cookieSyncConfig.MaxLimit) + if maxLimit < limit { + request.Limit = &maxLimit + } else { + request.Limit = &limit } - if cookieSyncConfig.MaxLimit != nil && (request.Limit <= 0 || request.Limit > *cookieSyncConfig.MaxLimit) { - request.Limit = *cookieSyncConfig.MaxLimit + return request +} + +func getEffectiveLimit(reqLimit *int, defaultLimit *int) int { + limit := reqLimit + + if limit == nil { + limit = defaultLimit } - if request.Limit < 0 { - request.Limit = 0 + + if limit != nil && *limit > 0 { + return *limit } - return request + return math.MaxInt +} + +func getEffectiveMaxLimit(maxLimit *int) int { + limit := maxLimit + + if limit != nil && *limit > 0 { + return *limit + } + + return math.MaxInt } func (c *cookieSyncEndpoint) setCooperativeSync(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest { @@ -395,8 +422,8 @@ func (c *cookieSyncEndpoint) writeSyncerMetrics(biddersEvaluated []usersync.Bidd c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncPrivacyBlocked) case usersync.StatusAlreadySynced: c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncAlreadySynced) - case usersync.StatusTypeNotSupported: - c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncTypeNotSupported) + case usersync.StatusRejectedByFilter: + c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncRejectedByFilter) } } } @@ -490,8 +517,8 @@ func getDebugMessage(status usersync.Status) string { return "Unsupported bidder" case usersync.StatusUnconfiguredBidder: return "No sync config" - case usersync.StatusTypeNotSupported: - return "Type not supported" + case usersync.StatusRejectedByFilter: + return "Rejected by request filter" case usersync.StatusBlockedByDisabledUsersync: return "Sync disabled by config" } @@ -503,7 +530,7 @@ type cookieSyncRequest struct { GDPR *int `json:"gdpr"` GDPRConsent string `json:"gdpr_consent"` USPrivacy string `json:"us_privacy"` - Limit int `json:"limit"` + Limit *int `json:"limit"` GPP string `json:"gpp"` GPPSID string `json:"gpp_sid"` CooperativeSync *bool `json:"coopSync"` diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index adfdcb22fab..bdc57322bf7 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "math" "net/http" "net/http/httptest" "strconv" @@ -631,6 +632,7 @@ func TestCookieSyncParseRequest(t *testing.T) { givenCCPAEnabled: true, expectedPrivacy: macros.UserSyncPrivacy{}, expectedRequest: usersync.Request{ + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -659,6 +661,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: true, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -687,6 +690,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: false, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -715,6 +719,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: false, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -743,6 +748,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: false, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -771,6 +777,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: true, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -799,6 +806,7 @@ func TestCookieSyncParseRequest(t *testing.T) { Enabled: true, PriorityGroups: [][]string{{"a", "b", "c"}}, }, + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -817,6 +825,7 @@ func TestCookieSyncParseRequest(t *testing.T) { givenCCPAEnabled: true, expectedPrivacy: macros.UserSyncPrivacy{}, expectedRequest: usersync.Request{ + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -837,6 +846,7 @@ func TestCookieSyncParseRequest(t *testing.T) { USPrivacy: "1NYN", }, expectedRequest: usersync.Request{ + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -878,6 +888,7 @@ func TestCookieSyncParseRequest(t *testing.T) { GDPR: "0", }, expectedRequest: usersync.Request{ + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -905,6 +916,7 @@ func TestCookieSyncParseRequest(t *testing.T) { GDPR: "", }, expectedRequest: usersync.Request{ + Limit: math.MaxInt, Privacy: usersyncPrivacy{ gdprPermissions: &fakePermissions{}, activityRequest: emptyActivityPoliciesRequest, @@ -1074,152 +1086,242 @@ func TestCookieSyncParseRequest(t *testing.T) { } } -func TestSetLimit(t *testing.T) { - intNegative1 := -1 - int20 := 20 - int30 := 30 - int40 := 40 +func TestGetEffectiveLimit(t *testing.T) { + intNegative := ptrutil.ToPtr(-1) + int0 := ptrutil.ToPtr(0) + int30 := ptrutil.ToPtr(30) + int40 := ptrutil.ToPtr(40) + intMax := ptrutil.ToPtr(math.MaxInt) + + tests := []struct { + name string + reqLimit *int + defaultLimit *int + expectedLimit int + }{ + { + name: "nil", + reqLimit: nil, + defaultLimit: nil, + expectedLimit: math.MaxInt, + }, + { + name: "req_limit_negative", + reqLimit: intNegative, + defaultLimit: nil, + expectedLimit: math.MaxInt, + }, + { + name: "req_limit_zero", + reqLimit: int0, + defaultLimit: nil, + expectedLimit: math.MaxInt, + }, + { + name: "req_limit_in_range", + reqLimit: int30, + defaultLimit: nil, + expectedLimit: 30, + }, + { + name: "req_limit_at_max", + reqLimit: intMax, + defaultLimit: nil, + expectedLimit: math.MaxInt, + }, + { + name: "default_limit_negative", + reqLimit: nil, + defaultLimit: intNegative, + expectedLimit: math.MaxInt, + }, + { + name: "default_limit_zero", + reqLimit: nil, + defaultLimit: intNegative, + expectedLimit: math.MaxInt, + }, + { + name: "default_limit_in_range", + reqLimit: nil, + defaultLimit: int30, + expectedLimit: 30, + }, + { + name: "default_limit_at_max", + reqLimit: nil, + defaultLimit: intMax, + expectedLimit: math.MaxInt, + }, + { + name: "both_in_range", + reqLimit: int30, + defaultLimit: int40, + expectedLimit: 30, + }, + } - testCases := []struct { - description string - givenRequest cookieSyncRequest - givenAccount *config.Account - expectedRequest cookieSyncRequest + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := getEffectiveLimit(test.reqLimit, test.defaultLimit) + assert.Equal(t, test.expectedLimit, result) + }) + } +} + +func TestGetEffectiveMaxLimit(t *testing.T) { + intNegative := ptrutil.ToPtr(-1) + int0 := ptrutil.ToPtr(0) + int30 := ptrutil.ToPtr(30) + intMax := ptrutil.ToPtr(math.MaxInt) + + tests := []struct { + name string + maxLimit *int + expectedLimit int }{ { - description: "Default Limit is Applied (request limit = 0)", - givenRequest: cookieSyncRequest{ - Limit: 0, - }, - givenAccount: &config.Account{ - CookieSync: config.CookieSync{ - DefaultLimit: &int20, - }, - }, - expectedRequest: cookieSyncRequest{ - Limit: 20, - }, + name: "nil", + maxLimit: nil, + expectedLimit: math.MaxInt, }, { - description: "Default Limit is Not Applied (default limit not set)", - givenRequest: cookieSyncRequest{ - Limit: 0, - }, - givenAccount: &config.Account{ - CookieSync: config.CookieSync{ - DefaultLimit: nil, - }, - }, - expectedRequest: cookieSyncRequest{ - Limit: 0, - }, + name: "req_limit_negative", + maxLimit: intNegative, + expectedLimit: math.MaxInt, }, { - description: "Default Limit is Not Applied (request limit > 0)", - givenRequest: cookieSyncRequest{ - Limit: 10, - }, - givenAccount: &config.Account{ - CookieSync: config.CookieSync{ - DefaultLimit: &int20, - }, - }, - expectedRequest: cookieSyncRequest{ - Limit: 10, - }, + name: "req_limit_zero", + maxLimit: int0, + expectedLimit: math.MaxInt, }, { - description: "Max Limit is Applied (request limit <= 0)", + name: "req_limit_in_range", + maxLimit: int30, + expectedLimit: 30, + }, + { + name: "req_limit_too_large", + maxLimit: intMax, + expectedLimit: math.MaxInt, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := getEffectiveMaxLimit(test.maxLimit) + assert.Equal(t, test.expectedLimit, result) + }) + } +} + +func TestSetLimit(t *testing.T) { + intNegative := ptrutil.ToPtr(-1) + int0 := ptrutil.ToPtr(0) + int10 := ptrutil.ToPtr(10) + int20 := ptrutil.ToPtr(20) + int30 := ptrutil.ToPtr(30) + intMax := ptrutil.ToPtr(math.MaxInt) + + tests := []struct { + name string + givenRequest cookieSyncRequest + givenAccount *config.Account + expectedRequest cookieSyncRequest + }{ + { + name: "nil_limits", givenRequest: cookieSyncRequest{ - Limit: 0, + Limit: nil, }, givenAccount: &config.Account{ CookieSync: config.CookieSync{ - MaxLimit: &int30, + DefaultLimit: nil, + MaxLimit: nil, }, }, expectedRequest: cookieSyncRequest{ - Limit: 30, + Limit: intMax, }, }, { - description: "Max Limit is Applied (0 < max < limit)", + name: "limit_negative", givenRequest: cookieSyncRequest{ - Limit: 40, + Limit: intNegative, }, givenAccount: &config.Account{ CookieSync: config.CookieSync{ - MaxLimit: &int30, + DefaultLimit: int20, }, }, expectedRequest: cookieSyncRequest{ - Limit: 30, + Limit: intMax, }, }, { - description: "Max Limit is Not Applied (max not set)", + name: "limit_zero", givenRequest: cookieSyncRequest{ - Limit: 10, + Limit: int0, }, givenAccount: &config.Account{ CookieSync: config.CookieSync{ - MaxLimit: nil, + DefaultLimit: int20, }, }, expectedRequest: cookieSyncRequest{ - Limit: 10, + Limit: intMax, }, }, { - description: "Max Limit is Not Applied (0 < limit < max)", + name: "limit_less_than_max", givenRequest: cookieSyncRequest{ - Limit: 10, + Limit: int10, }, givenAccount: &config.Account{ CookieSync: config.CookieSync{ - MaxLimit: &int30, + DefaultLimit: int20, + MaxLimit: int30, }, }, expectedRequest: cookieSyncRequest{ - Limit: 10, + Limit: int10, }, }, { - description: "Max Limit is Applied After applying the default", + name: "limit_greater_than_max", givenRequest: cookieSyncRequest{ - Limit: 0, + Limit: int30, }, givenAccount: &config.Account{ CookieSync: config.CookieSync{ - DefaultLimit: &int40, - MaxLimit: &int30, + DefaultLimit: int20, + MaxLimit: int10, }, }, expectedRequest: cookieSyncRequest{ - Limit: 30, + Limit: int10, }, }, { - description: "Negative Value Check", + name: "limit_at_max", givenRequest: cookieSyncRequest{ - Limit: 0, + Limit: intMax, }, givenAccount: &config.Account{ - CookieSync: config.CookieSync{ - DefaultLimit: &intNegative1, - MaxLimit: &intNegative1, - }, + CookieSync: config.CookieSync{}, }, expectedRequest: cookieSyncRequest{ - Limit: 0, + Limit: intMax, }, }, } - for _, test := range testCases { - endpoint := cookieSyncEndpoint{} - request := endpoint.setLimit(test.givenRequest, test.givenAccount.CookieSync) - assert.Equal(t, test.expectedRequest, request, test.description) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + endpoint := cookieSyncEndpoint{} + request := endpoint.setLimit(test.givenRequest, test.givenAccount.CookieSync) + assert.Equal(t, test.expectedRequest, request) + }) } } @@ -1581,10 +1683,10 @@ func TestCookieSyncWriteBidderMetrics(t *testing.T) { }, }, { - description: "One - Type Not Supported", - given: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusTypeNotSupported}}, + description: "One - Rejected By Filter", + given: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusRejectedByFilter}}, setExpectations: func(m *metrics.MetricsEngineMock) { - m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncTypeNotSupported).Once() + m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncRejectedByFilter).Once() }, }, { @@ -1638,7 +1740,7 @@ func TestCookieSyncHandleResponse(t *testing.T) { {Bidder: "Bidder2", Status: usersync.StatusUnknownBidder}, {Bidder: "Bidder3", Status: usersync.StatusUnconfiguredBidder}, {Bidder: "Bidder4", Status: usersync.StatusBlockedByPrivacy}, - {Bidder: "Bidder5", Status: usersync.StatusTypeNotSupported}, + {Bidder: "Bidder5", Status: usersync.StatusRejectedByFilter}, {Bidder: "Bidder6", Status: usersync.StatusBlockedByUserOptOut}, {Bidder: "Bidder7", Status: usersync.StatusBlockedByDisabledUsersync}, {Bidder: "BidderA", Status: usersync.StatusDuplicate, SyncerKey: "syncerB"}, @@ -1731,7 +1833,7 @@ func TestCookieSyncHandleResponse(t *testing.T) { givenCookieHasSyncs: true, givenDebug: true, givenSyncersChosen: []usersync.SyncerChoice{}, - expectedJSON: `{"status":"ok","bidder_status":[],"debug":[{"bidder":"Bidder1","error":"Already in sync"},{"bidder":"Bidder2","error":"Unsupported bidder"},{"bidder":"Bidder3","error":"No sync config"},{"bidder":"Bidder4","error":"Rejected by privacy"},{"bidder":"Bidder5","error":"Type not supported"},{"bidder":"Bidder6","error":"Status blocked by user opt out"},{"bidder":"Bidder7","error":"Sync disabled by config"},{"bidder":"BidderA","error":"Duplicate bidder synced as syncerB"}]}` + "\n", + expectedJSON: `{"status":"ok","bidder_status":[],"debug":[{"bidder":"Bidder1","error":"Already in sync"},{"bidder":"Bidder2","error":"Unsupported bidder"},{"bidder":"Bidder3","error":"No sync config"},{"bidder":"Bidder4","error":"Rejected by privacy"},{"bidder":"Bidder5","error":"Rejected by request filter"},{"bidder":"Bidder6","error":"Status blocked by user opt out"},{"bidder":"Bidder7","error":"Sync disabled by config"},{"bidder":"BidderA","error":"Duplicate bidder synced as syncerB"}]}` + "\n", expectedAnalytics: analytics.CookieSyncObject{Status: 200, BidderStatus: []*analytics.CookieSyncBidder{}}, }, } @@ -2151,9 +2253,9 @@ func (m *MockGDPRPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ex return args.Bool(0), args.Error(1) } -func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { +func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions { args := m.Called(ctx, bidderCoreName, bidder) - return args.Get(0).(gdpr.AuctionPermissions), args.Error(1) + return args.Get(0).(gdpr.AuctionPermissions) } type FakeAccountsFetcher struct { @@ -2183,10 +2285,10 @@ func (p *fakePermissions) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { +func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions { return gdpr.AuctionPermissions{ AllowBidRequest: true, - }, nil + } } func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy { diff --git a/endpoints/events/event.go b/endpoints/events/event.go index b92b72f17ad..6a4dd8b7096 100644 --- a/endpoints/events/event.go +++ b/endpoints/events/event.go @@ -216,7 +216,7 @@ func HandleAccountServiceErrors(errs []error) (status int, messages []string) { errCode := errortypes.ReadCode(er) - if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode { + if errCode == errortypes.BlockedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode { status = http.StatusServiceUnavailable } if errCode == errortypes.MalformedAcctErrorCode { diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index a6ad8d3fc65..e8e135acd39 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -59,7 +59,7 @@ type ORTB2 struct { func NewAmpEndpoint( uuidGenerator uuidutil.UUIDGenerator, ex exchange.Exchange, - validator openrtb_ext.BidderParamValidator, + requestValidator ortb.RequestValidator, requestsById stored_requests.Fetcher, accounts stored_requests.AccountFetcher, cfg *config.Configuration, @@ -73,7 +73,7 @@ func NewAmpEndpoint( tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { + if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { return nil, errors.New("NewAmpEndpoint requires non-nil arguments.") } @@ -87,7 +87,7 @@ func NewAmpEndpoint( return httprouter.Handle((&endpointDeps{ uuidGenerator, ex, - validator, + requestValidator, requestsById, empty_fetcher.EmptyFetcher{}, accounts, @@ -156,6 +156,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h w.Header().Set("AMP-Access-Control-Allow-Source-Origin", origin) w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) // There is no body for AMP requests, so we pass a nil body and ignore the return value. _, rejectErr := hookExecutor.ExecuteEntrypointStage(r, nilBody) @@ -171,7 +172,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h if errortypes.ContainsFatalError(errL) { w.WriteHeader(http.StatusBadRequest) for _, err := range errortypes.FatalOnly(errL) { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } labels.RequestStatus = metrics.RequestStatusBadInput return @@ -210,9 +211,9 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h metricsStatus := metrics.RequestStatusBadInput for _, er := range errL { errCode := errortypes.ReadCode(er) - if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode { + if errCode == errortypes.BlockedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode { httpStatus = http.StatusServiceUnavailable - metricsStatus = metrics.RequestStatusBlacklisted + metricsStatus = metrics.RequestStatusBlockedApp break } if errCode == errortypes.MalformedAcctErrorCode { @@ -224,12 +225,30 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h w.WriteHeader(httpStatus) labels.RequestStatus = metricsStatus for _, err := range errortypes.FatalOnly(errL) { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } ao.Errors = append(ao.Errors, acctIDErrs...) return } + // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). + if errs := deps.setFieldsImplicitly(r, reqWrapper, account); len(errs) > 0 { + errL = append(errL, errs...) + } + + hasStoredAuctionResponses := len(storedAuctionResponses) > 0 + errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredAuctionResponses, storedBidResponses, false) + errL = append(errL, errs...) + ao.Errors = append(ao.Errors, errs...) + if errortypes.ContainsFatalError(errs) { + w.WriteHeader(http.StatusBadRequest) + for _, err := range errortypes.FatalOnly(errs) { + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) + } + labels.RequestStatus = metrics.RequestStatusBadInput + return + } + tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) activityControl = privacy.NewActivityControl(&account.Privacy) @@ -385,8 +404,13 @@ func sendAmpResponse( ao.AmpTargetingValues = targets // Fixes #231 - enc := json.NewEncoder(w) + enc := json.NewEncoder(w) // nosemgrep: json-encoder-needs-type enc.SetEscapeHTML(false) + // Explicitly set content type to text/plain, which had previously been + // the implied behavior from the time the project was launched. + // It's unclear why text/plain was chosen or if it was an oversight, + // nevertheless we will keep it as such for compatibility reasons. + w.Header().Set("Content-Type", "text/plain; charset=utf-8") // If an error happens when encoding the response, there isn't much we can do. // If we've sent _any_ bytes, then Go would have sent the 200 status code first. @@ -423,6 +447,9 @@ func getExtBidResponse( warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage) } for _, v := range errortypes.WarningOnly(errs) { + if errortypes.ReadScope(v) == errortypes.ScopeDebug && !(reqWrapper != nil && reqWrapper.Test == 1) { + continue + } bidderErr := openrtb_ext.ExtBidderMessage{ Code: errortypes.ReadCode(v), Message: v.Error(), @@ -483,8 +510,13 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr // move to using the request wrapper req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal} - // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(httpRequest, req) + // normalize to openrtb 2.6 + if err := openrtb_ext.ConvertUpTo26(req); err != nil { + errs = append(errs, err) + } + if errortypes.ContainsFatalError(errs) { + return + } // Need to ensure cache and targeting are turned on e = initAmpTargetingAndCache(req) @@ -497,10 +529,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr return } - hasStoredResponses := len(storedAuctionResponses) > 0 - e = deps.validateRequest(req, true, hasStoredResponses, storedBidResponses, false) - errs = append(errs, e...) - return } @@ -514,7 +542,7 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req return nil, nil, nil, nil, []error{err} } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond) defer cancel() storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil) diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 7d87b65301d..e3533589859 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "strconv" + "strings" "testing" "time" @@ -30,6 +31,7 @@ import ( "github.com/prebid/prebid-server/v2/metrics" metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/v2/util/jsonutil" @@ -54,6 +56,7 @@ func TestGoodAmpRequests(t *testing.T) { "buyeruids-case-insensitive.json", "buyeruids-camel-case.json", "aliased-buyeruids-case-insensitive.json", + "ortb-2.5-to-2.6-upconvert.json", }, }, { @@ -101,8 +104,8 @@ func TestGoodAmpRequests(t *testing.T) { GDPR: config.GDPR{Enabled: true}, } if test.Config != nil { - cfg.BlacklistedApps = test.Config.BlacklistedApps - cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap() + cfg.BlockedApps = test.Config.BlockedApps + cfg.BlockedAppsLookup = test.Config.getBlockedAppLookup() cfg.AccountRequired = test.Config.AccountRequired } @@ -136,6 +139,17 @@ func TestGoodAmpRequests(t *testing.T) { assert.JSONEq(t, string(test.ExpectedValidatedBidReq), string(actualJson), "Not the expected validated request. Test file: %s", filename) } } + if test.ExpectedMockBidderRequests != nil { + for bidder, req := range test.ExpectedMockBidderRequests { + a, ok := ex.adapters[openrtb_ext.BidderName(bidder)] + if !ok { + t.Fatalf("Unexpected bidder %s has an expected mock bidder request. Test file: %s", bidder, filename) + } + aa := a.(*exchange.BidderAdapter) + ma := aa.Bidder.(*mockAdapter) + assert.JSONEq(t, string(req), string(ma.requestData[0]), "Not the expected mock bidder request for bidder %s. Test file: %s", bidder, filename) + } + } } } } @@ -201,7 +215,7 @@ func TestAMPPageInfo(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, exchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -233,55 +247,47 @@ func TestGDPRConsent(t *testing.T) { existingConsent := "BONV8oqONXwgmADACHENAO7pqzAAppY" testCases := []struct { - description string - consent string - userExt *openrtb_ext.ExtUser - nilUser bool - expectedUserExt openrtb_ext.ExtUser + description string + consent string + user *openrtb2.User + nilUser bool + expectedUser *openrtb2.User }{ { description: "Nil User", consent: consent, nilUser: true, - expectedUserExt: openrtb_ext.ExtUser{ - Consent: consent, - }, - }, - { - description: "Nil User Ext", - consent: consent, - userExt: nil, - expectedUserExt: openrtb_ext.ExtUser{ + expectedUser: &openrtb2.User{ Consent: consent, }, }, { description: "Overrides Existing Consent", consent: consent, - userExt: &openrtb_ext.ExtUser{ + user: &openrtb2.User{ Consent: existingConsent, }, - expectedUserExt: openrtb_ext.ExtUser{ + expectedUser: &openrtb2.User{ Consent: consent, }, }, { description: "Overrides Existing Consent - With Sibling Data", consent: consent, - userExt: &openrtb_ext.ExtUser{ + user: &openrtb2.User{ Consent: existingConsent, }, - expectedUserExt: openrtb_ext.ExtUser{ + expectedUser: &openrtb2.User{ Consent: consent, }, }, { description: "Does Not Override Existing Consent If Empty", consent: "", - userExt: &openrtb_ext.ExtUser{ + user: &openrtb2.User{ Consent: existingConsent, }, - expectedUserExt: openrtb_ext.ExtUser{ + expectedUser: &openrtb2.User{ Consent: existingConsent, }, }, @@ -289,7 +295,7 @@ func TestGDPRConsent(t *testing.T) { for _, test := range testCases { // Build Request - bid, err := getTestBidRequest(test.nilUser, test.userExt, true, nil) + bid, err := getTestBidRequest(test.nilUser, test.user, true, nil) if err != nil { t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) } @@ -299,10 +305,11 @@ func TestGDPRConsent(t *testing.T) { // Build Exchange Endpoint mockExchange := &mockAmpExchange{} + endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, mockExchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{ @@ -338,15 +345,8 @@ func TestGDPRConsent(t *testing.T) { if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { return } - if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { - return - } - var ue openrtb_ext.ExtUser - err = jsonutil.UnmarshalValid(result.User.Ext, &ue) - if !assert.NoError(t, err, test.description+":deserialize") { - return - } - assert.Equal(t, test.expectedUserExt, ue, test.description) + + assert.Equal(t, test.expectedUser, result.User, test.description) assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors, test.description+":errors") assert.Empty(t, response.ORTB2.Ext.Warnings, test.description+":warnings") @@ -369,15 +369,8 @@ func TestGDPRConsent(t *testing.T) { if !assert.NotNil(t, resultLegacy.User, test.description+":legacy:lastRequest.User") { return } - if !assert.NotNil(t, resultLegacy.User.Ext, test.description+":legacy:lastRequest.User.Ext") { - return - } - var ueLegacy openrtb_ext.ExtUser - err = jsonutil.UnmarshalValid(resultLegacy.User.Ext, &ueLegacy) - if !assert.NoError(t, err, test.description+":legacy:deserialize") { - return - } - assert.Equal(t, test.expectedUserExt, ueLegacy, test.description+":legacy") + + assert.Equal(t, test.expectedUser, resultLegacy.User, test.description+":legacy") assert.Equal(t, expectedErrorsFromHoldAuction, responseLegacy.ORTB2.Ext.Errors, test.description+":legacy:errors") assert.Empty(t, responseLegacy.ORTB2.Ext.Warnings, test.description+":legacy:warnings") } @@ -550,28 +543,6 @@ func TestOverrideWithParams(t *testing.T) { errorMsgs: []string{"unable to merge imp.ext with targeting data, check targeting data is correct: Invalid JSON Patch"}, }, }, - { - desc: "bid request with malformed user.ext.prebid - amp.Params with GDPR consent values - expect policy writer to return error", - given: testInput{ - ampParams: amp.Params{ - ConsentType: amp.ConsentTCF2, - Consent: "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", - }, - bidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, - User: &openrtb2.User{Ext: json.RawMessage(`{"prebid":{malformed}}`)}, - }, - }, - expected: testOutput{ - bidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, - User: &openrtb2.User{Ext: json.RawMessage(`{"prebid":{malformed}}`)}, - Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, - }, - errorMsgs: []string{"expect \" after {, but found m"}, - expectFatalErrors: true, - }, - }, } for _, test := range testCases { @@ -658,46 +629,46 @@ func TestCCPAConsent(t *testing.T) { var gdpr int8 = 1 testCases := []struct { - description string - consent string - regsExt *openrtb_ext.ExtRegs - nilRegs bool - expectedRegExt openrtb_ext.ExtRegs + description string + consent string + regs openrtb2.Regs + nilRegs bool + expectedReg *openrtb2.Regs }{ { description: "Nil Regs", consent: consent, nilRegs: true, - expectedRegExt: openrtb_ext.ExtRegs{ + expectedReg: &openrtb2.Regs{ USPrivacy: consent, }, }, { description: "Nil Regs Ext", consent: consent, - regsExt: nil, - expectedRegExt: openrtb_ext.ExtRegs{ + nilRegs: true, + expectedReg: &openrtb2.Regs{ USPrivacy: consent, }, }, { description: "Overrides Existing Consent", consent: consent, - regsExt: &openrtb_ext.ExtRegs{ + regs: openrtb2.Regs{ USPrivacy: existingConsent, }, - expectedRegExt: openrtb_ext.ExtRegs{ + expectedReg: &openrtb2.Regs{ USPrivacy: consent, }, }, { description: "Overrides Existing Consent - With Sibling Data", consent: consent, - regsExt: &openrtb_ext.ExtRegs{ + regs: openrtb2.Regs{ USPrivacy: existingConsent, GDPR: &gdpr, }, - expectedRegExt: openrtb_ext.ExtRegs{ + expectedReg: &openrtb2.Regs{ USPrivacy: consent, GDPR: &gdpr, }, @@ -705,10 +676,10 @@ func TestCCPAConsent(t *testing.T) { { description: "Does Not Override Existing Consent If Empty", consent: "", - regsExt: &openrtb_ext.ExtRegs{ + regs: openrtb2.Regs{ USPrivacy: existingConsent, }, - expectedRegExt: openrtb_ext.ExtRegs{ + expectedReg: &openrtb2.Regs{ USPrivacy: existingConsent, }, }, @@ -716,7 +687,7 @@ func TestCCPAConsent(t *testing.T) { for _, test := range testCases { // Build Request - bid, err := getTestBidRequest(true, nil, test.nilRegs, test.regsExt) + bid, err := getTestBidRequest(true, nil, test.nilRegs, &test.regs) if err != nil { t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) } @@ -729,7 +700,7 @@ func TestCCPAConsent(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, mockExchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -762,15 +733,8 @@ func TestCCPAConsent(t *testing.T) { if !assert.NotNil(t, result.Regs, test.description+":lastRequest.Regs") { return } - if !assert.NotNil(t, result.Regs.Ext, test.description+":lastRequest.Regs.Ext") { - return - } - var re openrtb_ext.ExtRegs - err = jsonutil.UnmarshalValid(result.Regs.Ext, &re) - if !assert.NoError(t, err, test.description+":deserialize") { - return - } - assert.Equal(t, test.expectedRegExt, re, test.description) + + assert.Equal(t, test.expectedReg, result.Regs, test.description) assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors) assert.Empty(t, response.ORTB2.Ext.Warnings) } @@ -778,7 +742,7 @@ func TestCCPAConsent(t *testing.T) { func TestConsentWarnings(t *testing.T) { type inputTest struct { - regs *openrtb_ext.ExtRegs + regs *openrtb2.Regs invalidConsentURL bool expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage } @@ -809,7 +773,7 @@ func TestConsentWarnings(t *testing.T) { expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning}}, }, { - regs: &openrtb_ext.ExtRegs{USPrivacy: "invalid"}, + regs: &openrtb2.Regs{USPrivacy: "invalid"}, invalidConsentURL: true, expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning, invalidConsentWarning}, @@ -817,7 +781,7 @@ func TestConsentWarnings(t *testing.T) { }, }, { - regs: &openrtb_ext.ExtRegs{USPrivacy: "1NYN"}, + regs: &openrtb2.Regs{USPrivacy: "1NYN"}, invalidConsentURL: false, expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderName("appnexus"): {bidderWarning}}, }, @@ -843,7 +807,7 @@ func TestConsentWarnings(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, mockExchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -900,17 +864,18 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { validConsentGDPR2 := "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" testCases := []struct { - description string - consent string - consentLegacy string - userExt *openrtb_ext.ExtUser - expectedUserExt openrtb_ext.ExtUser + description string + consent string + consentLegacy string + user *openrtb2.User + expectedUser *openrtb2.User }{ { description: "New Consent Wins", consent: validConsentGDPR1, consentLegacy: validConsentGDPR2, - expectedUserExt: openrtb_ext.ExtUser{ + user: &openrtb2.User{}, + expectedUser: &openrtb2.User{ Consent: validConsentGDPR1, }, }, @@ -918,7 +883,8 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { description: "New Consent Wins - Reverse", consent: validConsentGDPR2, consentLegacy: validConsentGDPR1, - expectedUserExt: openrtb_ext.ExtUser{ + user: &openrtb2.User{}, + expectedUser: &openrtb2.User{ Consent: validConsentGDPR2, }, }, @@ -926,7 +892,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { for _, test := range testCases { // Build Request - bid, err := getTestBidRequest(false, nil, true, nil) + bid, err := getTestBidRequest(false, test.user, true, nil) if err != nil { t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) } @@ -939,7 +905,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, mockExchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{ @@ -975,15 +941,8 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { return } - if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { - return - } - var ue openrtb_ext.ExtUser - err = jsonutil.UnmarshalValid(result.User.Ext, &ue) - if !assert.NoError(t, err, test.description+":deserialize") { - return - } - assert.Equal(t, test.expectedUserExt, ue, test.description) + + assert.Equal(t, test.expectedUser, result.User, test.description) assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors) assert.Empty(t, response.ORTB2.Ext.Warnings) } @@ -997,7 +956,7 @@ func TestAMPSiteExt(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, exchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1027,21 +986,43 @@ func TestAMPSiteExt(t *testing.T) { } // TestBadRequests makes sure we return 400's on bad requests. +// RTB26: Will need to be fixed once all validation functions are updated to rtb 2.6 func TestAmpBadRequests(t *testing.T) { - dir := "sample-requests/invalid-whole" + dir := "sample-requests/invalid-whole/" files, err := os.ReadDir(dir) assert.NoError(t, err, "Failed to read folder: %s", dir) - badRequests := make(map[string]json.RawMessage, len(files)) + mockAmpStoredReq := make(map[string]json.RawMessage, len(files)) + badRequests := make(map[string]testCase, len(files)) + filemap := make(map[string]string, len(files)) for index, file := range files { - badRequests[strconv.Itoa(100+index)] = readFile(t, "sample-requests/invalid-whole/"+file.Name()) + filename := file.Name() + fileData := readFile(t, dir+filename) + + test, err := parseTestData(fileData, filename) + if !assert.NoError(t, err) { + return + } + + if skipAmpTest(test) { + continue + } + + requestID := strconv.Itoa(100 + index) + test.Query = fmt.Sprintf("account=test_pub&tag_id=%s", requestID) + + badRequests[requestID] = test + mockAmpStoredReq[requestID] = test.BidRequest + filemap[requestID] = filename } + addAmpBadRequests(badRequests, mockAmpStoredReq) + endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, &mockAmpExchange{}, - newParamsValidator(t), - &mockAmpStoredReqFetcher{badRequests}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), + &mockAmpStoredReqFetcher{data: mockAmpStoredReq}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, &metricsConfig.NilMetricsEngine{}, @@ -1053,16 +1034,73 @@ func TestAmpBadRequests(t *testing.T) { hooks.EmptyPlanBuilder{}, nil, ) - for requestID := range badRequests { - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) + for id, test := range badRequests { + t.Run(filemap[id], func(t *testing.T) { + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil) + recorder := httptest.NewRecorder() + + endpoint(recorder, request, nil) + + response := recorder.Body.String() + assert.Equal(t, test.ExpectedReturnCode, recorder.Code, test.Description) + assert.Contains(t, response, test.ExpectedErrorMessage, "Actual: %s \nExpected: %s. Description: %s \n", response, test.ExpectedErrorMessage, test.Description) + }) + } +} - if recorder.Code != http.StatusBadRequest { - t.Errorf("Expected status %d. Got %d. Input was: %s", http.StatusBadRequest, recorder.Code, fmt.Sprintf("/openrtb2/auction/amp?config=%s", requestID)) +func skipAmpTest(test testCase) bool { + bidRequest := openrtb2.BidRequest{} + if err := json.Unmarshal(test.BidRequest, &bidRequest); err == nil { + // request.app must not exist in AMP + if bidRequest.App != nil { + return true } + + // data for tag_id='%s' does not define the required imp array + // Invalid request: data for tag_id '%s' includes %d imp elements. Only one is allowed + if len(bidRequest.Imp) == 0 || len(bidRequest.Imp) > 1 { + return true + } + + if bidRequest.Device != nil && strings.Contains(string(bidRequest.Device.Ext), "interstitial") { + return true + } + } + + // request.ext.prebid.cache is initialised in AMP if it is not present in request + if strings.Contains(test.ExpectedErrorMessage, `Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`) || + strings.Contains(test.ExpectedErrorMessage, `Invalid request: ext.prebid.storedrequest.id must be a string`) { + return true + } + + return false +} + +func addAmpBadRequests(mapBadRequests map[string]testCase, mockAmpStoredReq map[string]json.RawMessage) { + mapBadRequests["201"] = testCase{ + Description: "missing-tag-id", + Query: "account=test_pub", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: AMP requests require an AMP tag_id\n", + } + mockAmpStoredReq["201"] = json.RawMessage(`{}`) + + mapBadRequests["202"] = testCase{ + Description: "request.app-present", + Query: "account=test_pub&tag_id=202", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: request.app must not exist in AMP stored requests.\n", + } + mockAmpStoredReq["202"] = json.RawMessage(`{"imp":[{}],"app":{}}`) + + mapBadRequests["203"] = testCase{ + Description: "request-with-2-imps", + Query: "account=test_pub&tag_id=203", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: data for tag_id '203' includes 2 imp elements. Only one is allowed", } + mockAmpStoredReq["203"] = json.RawMessage(`{"imp":[{},{}]}`) } // TestAmpDebug makes sure we get debug information back when requested @@ -1074,7 +1112,7 @@ func TestAmpDebug(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, &mockAmpExchange{}, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1210,7 +1248,7 @@ func TestQueryParamOverrides(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, &mockAmpExchange{}, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1368,7 +1406,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, &mockAmpExchange{}, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1478,7 +1516,7 @@ func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r *exchange.A return &exchange.AuctionResponse{BidResponse: response}, nil } -func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, regsExt *openrtb_ext.ExtRegs) ([]byte, error) { +func getTestBidRequest(nilUser bool, user *openrtb2.User, nilRegs bool, regs *openrtb2.Regs) ([]byte, error) { var width int64 = 300 var height int64 = 300 bidRequest := &openrtb2.BidRequest{ @@ -1509,37 +1547,12 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, }, } - var userExtData []byte - if userExt != nil { - var err error - userExtData, err = jsonutil.Marshal(userExt) - if err != nil { - return nil, err - } - } - if !nilUser { - bidRequest.User = &openrtb2.User{ - ID: "aUserId", - BuyerUID: "aBuyerID", - Ext: userExtData, - } - } - - var regsExtData []byte - if regsExt != nil { - var err error - regsExtData, err = jsonutil.Marshal(regsExt) - if err != nil { - return nil, err - } + bidRequest.User = user } if !nilRegs { - bidRequest.Regs = &openrtb2.Regs{ - COPPA: 1, - Ext: regsExtData, - } + bidRequest.Regs = regs } return jsonutil.Marshal(bidRequest) } @@ -1657,6 +1670,7 @@ func (logger mockLogger) LogNotificationEventObject(uuidObj *analytics.Notificat func (logger mockLogger) LogAmpObject(ao *analytics.AmpObject, _ privacy.ActivityControl) { *logger.ampObject = *ao } +func (logger mockLogger) Shutdown() {} func TestBuildAmpObject(t *testing.T) { testCases := []struct { @@ -1909,7 +1923,7 @@ func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMe endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{id: "foo", err: nil}, exchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), mockAmpFetcher, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize, GenerateRequestID: generateRequestID}, @@ -1962,7 +1976,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, exchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{storedRequests}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1977,7 +1991,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) { ) for _, test := range testCases { - httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil) + httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp"+test.requestURLArguments, nil) recorder := httptest.NewRecorder() endpoint(recorder, httpReq, nil) @@ -1998,7 +2012,7 @@ func TestRequestWithTargeting(t *testing.T) { endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, exchange, - newParamsValidator(t), + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -2404,3 +2418,87 @@ func TestSetSeatNonBid(t *testing.T) { }) } } + +func TestAmpAuctionDebugWarningsOnly(t *testing.T) { + testCases := []struct { + description string + requestURLArguments string + addRequestHeaders func(r *http.Request) + expectedStatus int + expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage + }{ + { + description: "debug_enabled_request_with_invalid_Sec-Browsing-Topics_header", + requestURLArguments: "?tag_id=1&debug=1", + addRequestHeaders: func(r *http.Request) { + r.Header.Add("Sec-Browsing-Topics", "foo") + }, + expectedStatus: 200, + expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ + "general": { + { + Code: 10012, + Message: "Invalid field in Sec-Browsing-Topics header: foo", + }, + }, + }, + }, + { + description: "debug_disabled_request_with_invalid_Sec-Browsing-Topics_header", + requestURLArguments: "?tag_id=1", + addRequestHeaders: func(r *http.Request) { + r.Header.Add("Sec-Browsing-Topics", "foo") + }, + expectedStatus: 200, + expectedWarnings: nil, + }, + } + + storedRequests := map[string]json.RawMessage{ + "1": json.RawMessage(validRequest(t, "site.json")), + } + exchange := &nobidExchange{} + endpoint, _ := NewAmpEndpoint( + fakeUUIDGenerator{}, + exchange, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)), + &mockAmpStoredReqFetcher{storedRequests}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + TopicsDomain: "abc", + }, + }, + }, + }, + &metricsConfig.NilMetricsEngine{}, + analyticsBuild.New(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BuildBidderMap(), + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + ) + + for _, test := range testCases { + httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil) + test.addRequestHeaders(httpReq) + recorder := httptest.NewRecorder() + + endpoint(recorder, httpReq, nil) + + assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode) + + // Parse Response + var response AmpResponse + if err := jsonutil.UnmarshalValid(recorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } + + assert.Equal(t, test.expectedWarnings, response.ORTB2.Ext.Warnings) + } +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index c7beceb1b52..2ef86f13252 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -20,15 +20,14 @@ import ( "github.com/julienschmidt/httprouter" gpplib "github.com/prebid/go-gpp" "github.com/prebid/go-gpp/constants" - "github.com/prebid/openrtb/v20/adcom1" - "github.com/prebid/openrtb/v20/native1" - nativeRequests "github.com/prebid/openrtb/v20/native1/request" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/openrtb/v20/openrtb3" "github.com/prebid/prebid-server/v2/bidadjustment" "github.com/prebid/prebid-server/v2/hooks" "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" + "github.com/prebid/prebid-server/v2/privacysandbox" + "github.com/prebid/prebid-server/v2/schain" "golang.org/x/net/publicsuffix" jsonpatch "gopkg.in/evanphx/json-patch.v4" @@ -45,7 +44,6 @@ import ( "github.com/prebid/prebid-server/v2/prebid_cache_client" "github.com/prebid/prebid-server/v2/privacy/ccpa" "github.com/prebid/prebid-server/v2/privacy/lmt" - "github.com/prebid/prebid-server/v2/schain" "github.com/prebid/prebid-server/v2/stored_requests" "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/v2/stored_responses" @@ -57,12 +55,16 @@ import ( "github.com/prebid/prebid-server/v2/version" ) -const storedRequestTimeoutMillis = 50 const ampChannel = "amp" const appChannel = "app" +const secCookieDeprecation = "Sec-Cookie-Deprecation" +const secBrowsingTopics = "Sec-Browsing-Topics" +const observeBrowsingTopics = "Observe-Browsing-Topics" +const observeBrowsingTopicsValue = "?1" var ( dntKey string = http.CanonicalHeaderKey("DNT") + secGPCKey string = http.CanonicalHeaderKey("Sec-GPC") dntDisabled int8 = 0 dntEnabled int8 = 1 notAmp int8 = 0 @@ -84,7 +86,7 @@ var accountIdSearchPath = [...]struct { func NewEndpoint( uuidGenerator uuidutil.UUIDGenerator, ex exchange.Exchange, - validator openrtb_ext.BidderParamValidator, + requestValidator ortb.RequestValidator, requestsById stored_requests.Fetcher, accounts stored_requests.AccountFetcher, cfg *config.Configuration, @@ -97,7 +99,7 @@ func NewEndpoint( hookExecutionPlanBuilder hooks.ExecutionPlanBuilder, tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { + if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") } @@ -111,7 +113,7 @@ func NewEndpoint( return httprouter.Handle((&endpointDeps{ uuidGenerator, ex, - validator, + requestValidator, requestsById, empty_fetcher.EmptyFetcher{}, accounts, @@ -131,12 +133,10 @@ func NewEndpoint( openrtb_ext.NormalizeBidderName}).Auction), nil } -type normalizeBidderName func(name string) (openrtb_ext.BidderName, bool) - type endpointDeps struct { uuidGenerator uuidutil.UUIDGenerator ex exchange.Exchange - paramsValidator openrtb_ext.BidderParamValidator + requestValidator ortb.RequestValidator storedReqFetcher stored_requests.Fetcher videoFetcher stored_requests.Fetcher accounts stored_requests.AccountFetcher @@ -153,7 +153,7 @@ type endpointDeps struct { storedRespFetcher stored_requests.Fetcher hookExecutionPlanBuilder hooks.ExecutionPlanBuilder tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed - normalizeBidderName normalizeBidderName + normalizeBidderName openrtb_ext.BidderNameNormalizer } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -189,6 +189,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http }() w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) req, impExtInfoMap, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, account, errL := deps.parseRequest(r, &labels, hookExecutor) if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) { @@ -392,6 +393,13 @@ func sendAuctionResponse( return labels, ao } +// setBrowsingTopicsHeader always set the Observe-Browsing-Topics header to a value of ?1 if the Sec-Browsing-Topics is present in request +func setBrowsingTopicsHeader(w http.ResponseWriter, r *http.Request) { + if value := r.Header.Get(secBrowsingTopics); value != "" { + w.Header().Set(observeBrowsingTopics, observeBrowsingTopicsValue) + } +} + // parseRequest turns the HTTP request into an OpenRTB request. This is guaranteed to return: // // - A context which times out appropriately, given the request. @@ -405,6 +413,7 @@ func sendAuctionResponse( func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metrics.Labels, hookExecutor hookexecution.HookStageExecutor) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImpId stored_responses.BidderImpReplaceImpID, account *config.Account, errs []error) { errs = nil var err error + var errL []error var r io.ReadCloser = httpRequest.Body reqContentEncoding := httputil.ContentEncoding(httpRequest.Header.Get("Content-Encoding")) if reqContentEncoding != "" { @@ -455,7 +464,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric return } - timeout := parseTimeout(requestJson, time.Duration(storedRequestTimeoutMillis)*time.Millisecond) + timeout := parseTimeout(requestJson, time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -525,13 +534,21 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric return } + // normalize to openrtb 2.6 + if err := openrtb_ext.ConvertUpTo26(req); err != nil { + errs = []error{err} + return + } + if err := mergeBidderParams(req); err != nil { errs = []error{err} return } // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(httpRequest, req) + if errsL := deps.setFieldsImplicitly(httpRequest, req, account); len(errsL) > 0 { + errs = append(errs, errsL...) + } if err := ortb.SetDefaults(req); err != nil { errs = []error{err} @@ -546,13 +563,14 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric lmt.ModifyForIOS(req.BidRequest) //Stored auction responses should be processed after stored requests due to possible impression modification - storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errs = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher) - if len(errs) > 0 { + storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errL = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher) + if len(errL) > 0 { + errs = append(errs, errL...) return nil, nil, nil, nil, nil, nil, errs } - hasStoredResponses := len(storedAuctionResponses) > 0 - errL := deps.validateRequest(req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) + hasStoredAuctionResponses := len(storedAuctionResponses) > 0 + errL = deps.validateRequest(account, httpRequest, req, false, hasStoredAuctionResponses, storedBidResponses, hasStoredBidRequest) if len(errL) > 0 { errs = append(errs, errL...) } @@ -657,7 +675,7 @@ func mergeBidderParamsImpExt(impExt *openrtb_ext.ImpExt, reqExtParams map[string extMapModified := false for bidder, params := range reqExtParams { - if !isPossibleBidder(bidder) { + if !openrtb_ext.IsPotentialBidder(bidder) { continue } @@ -746,7 +764,7 @@ func mergeBidderParamsImpExtPrebid(impExt *openrtb_ext.ImpExt, reqExtParams map[ return nil } -func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { +func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { errL := []error{} if req.ID == "" { return []error{errors.New("request missing required field: \"id\"")} @@ -812,10 +830,6 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp } } - if err := mapSChains(req); err != nil { - return []error{err} - } - if err := validateOrFillChannel(req, isAmp); err != nil { return []error{err} } @@ -875,6 +889,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp return append(errL, err) } + if err := validateOrFillCookieDeprecation(httpReq, req, account); err != nil { + errL = append(errL, err) + } + if ccpaPolicy, err := ccpa.ReadFromRequestWrapper(req, gpp); err != nil { errL = append(errL, err) if errortypes.ContainsFatalError([]error{err}) { @@ -903,7 +921,7 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp } impIDs[imp.ID] = i - errs := deps.validateImp(imp, requestAliases, i, hasStoredResponses, storedBidResp) + errs := deps.requestValidator.ValidateImp(imp, ortb.ValidationConfig{}, i, requestAliases, hasStoredAuctionResponses, storedBidResp) if len(errs) > 0 { errL = append(errL, errs...) } @@ -915,32 +933,6 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp return errL } -// mapSChains maps an schain defined in an ORTB 2.4 location (req.ext.schain) to the ORTB 2.5 location -// (req.source.ext.schain) if no ORTB 2.5 schain (req.source.ext.schain, req.ext.prebid.schains) exists. -// An ORTB 2.4 schain is always deleted from the 2.4 location regardless of whether an ORTB 2.5 schain exists. -func mapSChains(req *openrtb_ext.RequestWrapper) error { - reqExt, err := req.GetRequestExt() - if err != nil { - return fmt.Errorf("req.ext is invalid: %v", err) - } - sourceExt, err := req.GetSourceExt() - if err != nil { - return fmt.Errorf("source.ext is invalid: %v", err) - } - - reqExtSChain := reqExt.GetSChain() - reqExt.SetSChain(nil) - - if reqPrebid := reqExt.GetPrebid(); reqPrebid != nil && reqPrebid.SChains != nil { - return nil - } else if sourceExt.GetSChain() != nil { - return nil - } else if reqExtSChain != nil { - sourceExt.SetSChain(reqExtSChain) - } - return nil -} - func validateAndFillSourceTID(req *openrtb_ext.RequestWrapper, generateRequestID bool, hasStoredBidRequest bool, isAmp bool) error { if req.Source == nil { req.Source = &openrtb2.Source{} @@ -1048,558 +1040,6 @@ func (deps *endpointDeps) validateBidders(bidders []string, knownBidders map[str return nil } -func (deps *endpointDeps) validateImp(imp *openrtb_ext.ImpWrapper, aliases map[string]string, index int, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error { - if imp.ID == "" { - return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)} - } - - if len(imp.Metric) != 0 { - return []error{fmt.Errorf("request.imp[%d].metric is not yet supported by prebid-server. Support may be added in the future", index)} - } - - if imp.Banner == nil && imp.Video == nil && imp.Audio == nil && imp.Native == nil { - return []error{fmt.Errorf("request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", index)} - } - - if err := validateBanner(imp.Banner, index, isInterstitial(imp)); err != nil { - return []error{err} - } - - if err := validateVideo(imp.Video, index); err != nil { - return []error{err} - } - - if err := validateAudio(imp.Audio, index); err != nil { - return []error{err} - } - - if err := fillAndValidateNative(imp.Native, index); err != nil { - return []error{err} - } - - if err := validatePmp(imp.PMP, index); err != nil { - return []error{err} - } - - errL := deps.validateImpExt(imp, aliases, index, hasStoredResponses, storedBidResp) - if len(errL) != 0 { - return errL - } - - return nil -} - -func isInterstitial(imp *openrtb_ext.ImpWrapper) bool { - return imp.Instl == 1 -} - -func validateBanner(banner *openrtb2.Banner, impIndex int, isInterstitial bool) error { - if banner == nil { - return nil - } - - // The following fields were previously uints in the OpenRTB library we use, but have - // since been changed to ints. We decided to maintain the non-negative check. - if banner.W != nil && *banner.W < 0 { - return fmt.Errorf("request.imp[%d].banner.w must be a positive number", impIndex) - } - if banner.H != nil && *banner.H < 0 { - return fmt.Errorf("request.imp[%d].banner.h must be a positive number", impIndex) - } - - // The following fields are deprecated in the OpenRTB 2.5 spec but are still present - // in the OpenRTB library we use. Enforce they are not specified. - if banner.WMin != 0 { - return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmin\". Use the \"format\" array instead.", impIndex) - } - if banner.WMax != 0 { - return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmax\". Use the \"format\" array instead.", impIndex) - } - if banner.HMin != 0 { - return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmin\". Use the \"format\" array instead.", impIndex) - } - if banner.HMax != 0 { - return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmax\". Use the \"format\" array instead.", impIndex) - } - - hasRootSize := banner.H != nil && banner.W != nil && *banner.H > 0 && *banner.W > 0 - if !hasRootSize && len(banner.Format) == 0 && !isInterstitial { - return fmt.Errorf("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.", impIndex) - } - - for i, format := range banner.Format { - if err := validateFormat(&format, impIndex, i); err != nil { - return err - } - } - - return nil -} - -func validateVideo(video *openrtb2.Video, impIndex int) error { - if video == nil { - return nil - } - - if len(video.MIMEs) < 1 { - return fmt.Errorf("request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex) - } - - // The following fields were previously uints in the OpenRTB library we use, but have - // since been changed to ints. We decided to maintain the non-negative check. - if video.W != nil && *video.W < 0 { - return fmt.Errorf("request.imp[%d].video.w must be a positive number", impIndex) - } - if video.H != nil && *video.H < 0 { - return fmt.Errorf("request.imp[%d].video.h must be a positive number", impIndex) - } - if video.MinBitRate < 0 { - return fmt.Errorf("request.imp[%d].video.minbitrate must be a positive number", impIndex) - } - if video.MaxBitRate < 0 { - return fmt.Errorf("request.imp[%d].video.maxbitrate must be a positive number", impIndex) - } - - return nil -} - -func validateAudio(audio *openrtb2.Audio, impIndex int) error { - if audio == nil { - return nil - } - - if len(audio.MIMEs) < 1 { - return fmt.Errorf("request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex) - } - - // The following fields were previously uints in the OpenRTB library we use, but have - // since been changed to ints. We decided to maintain the non-negative check. - if audio.Sequence < 0 { - return fmt.Errorf("request.imp[%d].audio.sequence must be a positive number", impIndex) - } - if audio.MaxSeq < 0 { - return fmt.Errorf("request.imp[%d].audio.maxseq must be a positive number", impIndex) - } - if audio.MinBitrate < 0 { - return fmt.Errorf("request.imp[%d].audio.minbitrate must be a positive number", impIndex) - } - if audio.MaxBitrate < 0 { - return fmt.Errorf("request.imp[%d].audio.maxbitrate must be a positive number", impIndex) - } - - return nil -} - -// fillAndValidateNative validates the request, and assigns the Asset IDs as recommended by the Native v1.2 spec. -func fillAndValidateNative(n *openrtb2.Native, impIndex int) error { - if n == nil { - return nil - } - - if len(n.Request) == 0 { - return fmt.Errorf("request.imp[%d].native missing required property \"request\"", impIndex) - } - var nativePayload nativeRequests.Request - if err := jsonutil.UnmarshalValid(json.RawMessage(n.Request), &nativePayload); err != nil { - return err - } - - if err := validateNativeContextTypes(nativePayload.Context, nativePayload.ContextSubType, impIndex); err != nil { - return err - } - if err := validateNativePlacementType(nativePayload.PlcmtType, impIndex); err != nil { - return err - } - if err := fillAndValidateNativeAssets(nativePayload.Assets, impIndex); err != nil { - return err - } - if err := validateNativeEventTrackers(nativePayload.EventTrackers, impIndex); err != nil { - return err - } - - serialized, err := jsonutil.Marshal(nativePayload) - if err != nil { - return err - } - n.Request = string(serialized) - return nil -} - -func validateNativeContextTypes(cType native1.ContextType, cSubtype native1.ContextSubType, impIndex int) error { - if cType == 0 { - // Context is only recommended, so none is a valid type. - return nil - } - if cType < native1.ContextTypeContent || (cType > native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound) { - return fmt.Errorf("request.imp[%d].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex) - } - if cSubtype < 0 { - return fmt.Errorf("request.imp[%d].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex) - } - if cSubtype == 0 { - return nil - } - if cSubtype >= native1.ContextSubTypeGeneral && cSubtype <= native1.ContextSubTypeUserGenerated { - if cType != native1.ContextTypeContent && cType < openrtb_ext.NativeExchangeSpecificLowerBound { - return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype) - } - return nil - } - if cSubtype >= native1.ContextSubTypeSocial && cSubtype <= native1.ContextSubTypeChat { - if cType != native1.ContextTypeSocial && cType < openrtb_ext.NativeExchangeSpecificLowerBound { - return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype) - } - return nil - } - if cSubtype >= native1.ContextSubTypeSelling && cSubtype <= native1.ContextSubTypeProductReview { - if cType != native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound { - return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype) - } - return nil - } - if cSubtype >= openrtb_ext.NativeExchangeSpecificLowerBound { - return nil - } - - return fmt.Errorf("request.imp[%d].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex) -} - -func validateNativePlacementType(pt native1.PlacementType, impIndex int) error { - if pt == 0 { - // Placement Type is only recommended, not required. - return nil - } - if pt < native1.PlacementTypeFeed || (pt > native1.PlacementTypeRecommendationWidget && pt < openrtb_ext.NativeExchangeSpecificLowerBound) { - return fmt.Errorf("request.imp[%d].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex) - } - return nil -} - -func fillAndValidateNativeAssets(assets []nativeRequests.Asset, impIndex int) error { - if len(assets) < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets must be an array containing at least one object", impIndex) - } - - assetIDs := make(map[int64]struct{}, len(assets)) - - // If none of the asset IDs are defined by the caller, then prebid server should assign its own unique IDs. But - // if the caller did assign its own asset IDs, then prebid server will respect those IDs - assignAssetIDs := true - for i := 0; i < len(assets); i++ { - assignAssetIDs = assignAssetIDs && (assets[i].ID == 0) - } - - for i := 0; i < len(assets); i++ { - if err := validateNativeAsset(assets[i], impIndex, i); err != nil { - return err - } - - if assignAssetIDs { - assets[i].ID = int64(i) - continue - } - - // Each asset should have a unique ID thats assigned by the caller - if _, ok := assetIDs[assets[i].ID]; ok { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].id is already being used by another asset. Each asset ID must be unique.", impIndex, i) - } - - assetIDs[assets[i].ID] = struct{}{} - } - - return nil -} - -func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex int) error { - assetErr := "request.imp[%d].native.request.assets[%d] must define exactly one of {title, img, video, data}" - foundType := false - - if asset.Title != nil { - foundType = true - if err := validateNativeAssetTitle(asset.Title, impIndex, assetIndex); err != nil { - return err - } - } - - if asset.Img != nil { - if foundType { - return fmt.Errorf(assetErr, impIndex, assetIndex) - } - foundType = true - if err := validateNativeAssetImage(asset.Img, impIndex, assetIndex); err != nil { - return err - } - } - - if asset.Video != nil { - if foundType { - return fmt.Errorf(assetErr, impIndex, assetIndex) - } - foundType = true - if err := validateNativeAssetVideo(asset.Video, impIndex, assetIndex); err != nil { - return err - } - } - - if asset.Data != nil { - if foundType { - return fmt.Errorf(assetErr, impIndex, assetIndex) - } - foundType = true - if err := validateNativeAssetData(asset.Data, impIndex, assetIndex); err != nil { - return err - } - } - - if !foundType { - return fmt.Errorf(assetErr, impIndex, assetIndex) - } - - return nil -} - -func validateNativeEventTrackers(trackers []nativeRequests.EventTracker, impIndex int) error { - for i := 0; i < len(trackers); i++ { - if err := validateNativeEventTracker(trackers[i], impIndex, i); err != nil { - return err - } - } - return nil -} - -func validateNativeAssetTitle(title *nativeRequests.Title, impIndex int, assetIndex int) error { - if title.Len < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].title.len must be a positive number", impIndex, assetIndex) - } - return nil -} - -func validateNativeEventTracker(tracker nativeRequests.EventTracker, impIndex int, eventIndex int) error { - if tracker.Event < native1.EventTypeImpression || (tracker.Event > native1.EventTypeViewableVideo50 && tracker.Event < openrtb_ext.NativeExchangeSpecificLowerBound) { - return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex) - } - if len(tracker.Methods) < 1 { - return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex) - } - for methodIndex, method := range tracker.Methods { - if method < native1.EventTrackingMethodImage || (method > native1.EventTrackingMethodJS && method < openrtb_ext.NativeExchangeSpecificLowerBound) { - return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex, methodIndex) - } - } - - return nil -} - -func validateNativeAssetImage(img *nativeRequests.Image, impIndex int, assetIndex int) error { - if img.W < 0 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.w must be a positive integer", impIndex, assetIndex) - } - if img.H < 0 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.h must be a positive integer", impIndex, assetIndex) - } - if img.WMin < 0 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.wmin must be a positive integer", impIndex, assetIndex) - } - if img.HMin < 0 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.hmin must be a positive integer", impIndex, assetIndex) - } - return nil -} - -func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error { - if len(video.MIMEs) < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type", impIndex, assetIndex) - } - if video.MinDuration < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", impIndex, assetIndex) - } - if video.MaxDuration < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", impIndex, assetIndex) - } - if err := validateNativeVideoProtocols(video.Protocols, impIndex, assetIndex); err != nil { - return err - } - - return nil -} - -func validateNativeAssetData(data *nativeRequests.Data, impIndex int, assetIndex int) error { - if data.Type < native1.DataAssetTypeSponsored || (data.Type > native1.DataAssetTypeCTAText && data.Type < 500) { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex, assetIndex) - } - - return nil -} - -func validateNativeVideoProtocols(protocols []adcom1.MediaCreativeSubtype, impIndex int, assetIndex int) error { - if len(protocols) < 1 { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least one element", impIndex, assetIndex) - } - for i := 0; i < len(protocols); i++ { - if err := validateNativeVideoProtocol(protocols[i], impIndex, assetIndex, i); err != nil { - return err - } - } - return nil -} - -func validateNativeVideoProtocol(protocol adcom1.MediaCreativeSubtype, impIndex int, assetIndex int, protocolIndex int) error { - if protocol < adcom1.CreativeVAST10 || protocol > adcom1.CreativeDAAST10Wrapper { - return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols[%d] is invalid. See Section 5.8: https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=52", impIndex, assetIndex, protocolIndex) - } - return nil -} - -func validateFormat(format *openrtb2.Format, impIndex, formatIndex int) error { - usesHW := format.W != 0 || format.H != 0 - usesRatios := format.WMin != 0 || format.WRatio != 0 || format.HRatio != 0 - - // The following fields were previously uints in the OpenRTB library we use, but have - // since been changed to ints. We decided to maintain the non-negative check. - if format.W < 0 { - return fmt.Errorf("request.imp[%d].banner.format[%d].w must be a positive number", impIndex, formatIndex) - } - if format.H < 0 { - return fmt.Errorf("request.imp[%d].banner.format[%d].h must be a positive number", impIndex, formatIndex) - } - if format.WRatio < 0 { - return fmt.Errorf("request.imp[%d].banner.format[%d].wratio must be a positive number", impIndex, formatIndex) - } - if format.HRatio < 0 { - return fmt.Errorf("request.imp[%d].banner.format[%d].hratio must be a positive number", impIndex, formatIndex) - } - if format.WMin < 0 { - return fmt.Errorf("request.imp[%d].banner.format[%d].wmin must be a positive number", impIndex, formatIndex) - } - - if usesHW && usesRatios { - return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" objects in the request.", impIndex, formatIndex) - } - if !usesHW && !usesRatios { - return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero.", impIndex, formatIndex) - } - if usesHW && (format.W == 0 || format.H == 0) { - return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"h\" and \"w\" properties.", impIndex, formatIndex) - } - if usesRatios && (format.WMin == 0 || format.WRatio == 0 || format.HRatio == 0) { - return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"wmin\", \"wratio\", and \"hratio\" properties.", impIndex, formatIndex) - } - return nil -} - -func validatePmp(pmp *openrtb2.PMP, impIndex int) error { - if pmp == nil { - return nil - } - - for dealIndex, deal := range pmp.Deals { - if deal.ID == "" { - return fmt.Errorf("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", impIndex, dealIndex) - } - } - return nil -} - -func (deps *endpointDeps) validateImpExt(imp *openrtb_ext.ImpWrapper, aliases map[string]string, impIndex int, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error { - if len(imp.Ext) == 0 { - return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)} - } - - impExt, err := imp.GetImpExt() - if err != nil { - return []error{err} - } - - prebid := impExt.GetOrCreatePrebid() - prebidModified := false - - if prebid.Bidder == nil { - prebid.Bidder = make(map[string]json.RawMessage) - } - - ext := impExt.GetExt() - extModified := false - - // promote imp[].ext.BIDDER to newer imp[].ext.prebid.bidder.BIDDER location, with the later taking precedence - for k, v := range ext { - if isPossibleBidder(k) { - if _, exists := prebid.Bidder[k]; !exists { - prebid.Bidder[k] = v - prebidModified = true - } - delete(ext, k) - extModified = true - } - } - - if hasStoredResponses && prebid.StoredAuctionResponse == nil { - return []error{fmt.Errorf("request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[%d].ext.prebid \n", impIndex)} - } - - if err := deps.validateStoredBidRespAndImpExtBidders(prebid, storedBidResp, imp.ID); err != nil { - return []error{err} - } - - errL := []error{} - - for bidder, ext := range prebid.Bidder { - coreBidder, _ := openrtb_ext.NormalizeBidderName(bidder) - if tmp, isAlias := aliases[bidder]; isAlias { - coreBidder = openrtb_ext.BidderName(tmp) - } - - if coreBidderNormalized, isValid := deps.bidderMap[coreBidder.String()]; isValid { - if err := deps.paramsValidator.Validate(coreBidderNormalized, ext); err != nil { - return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)} - } - } else { - if msg, isDisabled := deps.disabledBidders[bidder]; isDisabled { - errL = append(errL, &errortypes.BidderTemporarilyDisabled{Message: msg}) - delete(prebid.Bidder, bidder) - prebidModified = true - } else { - return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder)} - } - } - } - - if len(prebid.Bidder) == 0 { - errL = append(errL, fmt.Errorf("request.imp[%d].ext.prebid.bidder must contain at least one bidder", impIndex)) - return errL - } - - if prebidModified { - impExt.SetPrebid(prebid) - } - if extModified { - impExt.SetExt(ext) - } - - return errL -} - -// isPossibleBidder determines if a bidder name is a potential real bidder. -func isPossibleBidder(bidder string) bool { - switch openrtb_ext.BidderName(bidder) { - case openrtb_ext.BidderReservedContext: - return false - case openrtb_ext.BidderReservedData: - return false - case openrtb_ext.BidderReservedGPID: - return false - case openrtb_ext.BidderReservedPrebid: - return false - case openrtb_ext.BidderReservedSKAdN: - return false - case openrtb_ext.BidderReservedTID: - return false - case openrtb_ext.BidderReservedAE: - return false - default: - return true - } -} - func (deps *endpointDeps) parseBidExt(req *openrtb_ext.RequestWrapper) error { if _, err := req.GetRequestExt(); err != nil { return fmt.Errorf("request.ext is invalid: %v", err) @@ -1692,36 +1132,28 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) []error { } func validateTargeting(t *openrtb_ext.ExtRequestTargeting) error { - if t == nil { - return nil - } - - if (t.IncludeWinners == nil || !*t.IncludeWinners) && (t.IncludeBidderKeys == nil || !*t.IncludeBidderKeys) { - return errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support") - } - - if t.PriceGranularity != nil { - if err := validatePriceGranularity(t.PriceGranularity); err != nil { - return err + if t != nil { + if t.PriceGranularity != nil { + if err := validatePriceGranularity(t.PriceGranularity); err != nil { + return err + } } - } - - if t.MediaTypePriceGranularity.Video != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Video); err != nil { - return err + if t.MediaTypePriceGranularity.Video != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Video); err != nil { + return err + } } - } - if t.MediaTypePriceGranularity.Banner != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Banner); err != nil { - return err + if t.MediaTypePriceGranularity.Banner != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Banner); err != nil { + return err + } } - } - if t.MediaTypePriceGranularity.Native != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Native); err != nil { - return err + if t.MediaTypePriceGranularity.Native != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Native); err != nil { + return err + } } } - return nil } @@ -1774,8 +1206,8 @@ func (deps *endpointDeps) validateApp(req *openrtb_ext.RequestWrapper) error { } if req.App.ID != "" { - if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found { - return &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} + if _, found := deps.cfg.BlockedAppsLookup[req.App.ID]; found { + return &errortypes.BlockedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} } } @@ -1820,6 +1252,7 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases if err != nil { return append(errL, fmt.Errorf("request.user.ext object is not valid: %v", err)) } + // Check if the buyeruids are valid prebid := userExt.GetPrebid() if prebid != nil { @@ -1836,28 +1269,20 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases } } } + // Check Universal User ID - eids := userExt.GetEid() - if eids != nil { - eidsValue := *eids - uniqueSources := make(map[string]struct{}, len(eidsValue)) - for eidIndex, eid := range eidsValue { - if eid.Source == "" { - return append(errL, fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex)) - } - if _, ok := uniqueSources[eid.Source]; ok { - return append(errL, errors.New("request.user.ext.eids must contain unique sources")) - } - uniqueSources[eid.Source] = struct{}{} + for eidIndex, eid := range req.User.EIDs { + if eid.Source == "" { + return append(errL, fmt.Errorf("request.user.eids[%d] missing required field: \"source\"", eidIndex)) + } - if len(eid.UIDs) == 0 { - return append(errL, fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex)) - } + if len(eid.UIDs) == 0 { + return append(errL, fmt.Errorf("request.user.eids[%d].uids must contain at least one element or be undefined", eidIndex)) + } - for uidIndex, uid := range eid.UIDs { - if uid.ID == "" { - return append(errL, fmt.Errorf("request.user.ext.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex)) - } + for uidIndex, uid := range eid.UIDs { + if uid.ID == "" { + return append(errL, fmt.Errorf("request.user.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex)) } } } @@ -1886,16 +1311,11 @@ func validateRegs(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) []er WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) } } - regsExt, err := req.GetRegExt() - if err != nil { - return append(errL, fmt.Errorf("request.regs.ext is invalid: %v", err)) - } - gdpr := regsExt.GetGDPR() - if gdpr != nil && *gdpr != 0 && *gdpr != 1 { - return append(errL, errors.New("request.regs.ext.gdpr must be either 0 or 1")) + reqGDPR := req.BidRequest.Regs.GDPR + if reqGDPR != nil && *reqGDPR != 0 && *reqGDPR != 1 { + return append(errL, errors.New("request.regs.gdpr must be either 0 or 1")) } - return errL } @@ -1918,7 +1338,35 @@ func validateDevice(device *openrtb2.Device) error { if device.Geo != nil && device.Geo.Accuracy < 0 { return errors.New("request.device.geo.accuracy must be a positive number") } + return nil +} + +func validateOrFillCookieDeprecation(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error { + if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { + return nil + } + + deviceExt, err := req.GetDeviceExt() + if err != nil { + return err + } + if deviceExt.GetCDep() != "" { + return nil + } + + secCookieDeprecation := httpReq.Header.Get(secCookieDeprecation) + if secCookieDeprecation == "" { + return nil + } + if len(secCookieDeprecation) > 100 { + return &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + } + } + + deviceExt.SetCDep(secCookieDeprecation) return nil } @@ -2003,7 +1451,7 @@ func sanitizeRequest(r *openrtb_ext.RequestWrapper, ipValidator iputil.IPValidat // OpenRTB properties from the headers and other implicit info. // // This function _should not_ override any fields which were defined explicitly by the caller in the request. -func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { +func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error { sanitizeRequest(r, deps.privateNetworkIPValidator) setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator) @@ -2015,6 +1463,14 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ } setAuctionTypeImplicitly(r) + + err := setGPCImplicitly(httpReq, r) + if err != nil { + return []error{err} + } + + errs := setSecBrowsingTopicsImplicitly(httpReq, r, account) + return errs } // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device @@ -2022,7 +1478,6 @@ func setDeviceImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, i setIPImplicitly(httpReq, r, ipValidtor) setUAImplicitly(httpReq, r) setDoNotTrackImplicitly(httpReq, r) - } // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request, @@ -2033,6 +1488,53 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) { } } +func setGPCImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) error { + secGPC := httpReq.Header.Get(secGPCKey) + + if secGPC != "1" { + return nil + } + + regExt, err := r.GetRegExt() + if err != nil { + return err + } + + if regExt.GetGPC() != nil { + return nil + } + + gpc := "1" + regExt.SetGPC(&gpc) + + return nil +} + +// setSecBrowsingTopicsImplicitly updates user.data with data from request header 'Sec-Browsing-Topics' +func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error { + secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics) + if secBrowsingTopics == "" { + return nil + } + + // host must configure privacy sandbox + if account == nil || account.Privacy.PrivacySandbox.TopicsDomain == "" { + return nil + } + + topics, errs := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics) + if len(topics) == 0 { + return errs + } + + if r.User == nil { + r.User = &openrtb2.User{} + } + + r.User.Data = privacysandbox.UpdateUserDataWithTopics(r.User.Data, topics, account.Privacy.PrivacySandbox.TopicsDomain) + return errs +} + func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { if r.Site == nil { r.Site = &openrtb2.Site{} @@ -2254,8 +1756,8 @@ func (deps *endpointDeps) processStoredRequests(requestJson []byte, impInfo []Im // parseImpInfo parses the request JSON and returns impression and unmarshalled imp.ext.prebid func parseImpInfo(requestJson []byte) (impData []ImpExtPrebidData, errs []error) { if impArray, dataType, _, err := jsonparser.Get(requestJson, "imp"); err == nil && dataType == jsonparser.Array { - _, err = jsonparser.ArrayEach(impArray, func(imp []byte, _ jsonparser.ValueType, _ int, err error) { - impExtData, _, _, err := jsonparser.Get(imp, "ext", "prebid") + _, _ = jsonparser.ArrayEach(impArray, func(imp []byte, _ jsonparser.ValueType, _ int, _ error) { + impExtData, _, _, _ := jsonparser.Get(imp, "ext", "prebid") var impExtPrebid openrtb_ext.ExtImpPrebid if impExtData != nil { if err := jsonutil.Unmarshal(impExtData, &impExtPrebid); err != nil { @@ -2362,9 +1864,9 @@ func writeError(errs []error, w http.ResponseWriter, labels *metrics.Labels) boo metricsStatus := metrics.RequestStatusBadInput for _, err := range errs { erVal := errortypes.ReadCode(err) - if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode { + if erVal == errortypes.BlockedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode { httpStatus = http.StatusServiceUnavailable - metricsStatus = metrics.RequestStatusBlacklisted + metricsStatus = metrics.RequestStatusBlockedApp break } else if erVal == errortypes.MalformedAcctErrorCode { httpStatus = http.StatusInternalServerError @@ -2375,7 +1877,7 @@ func writeError(errs []error, w http.ResponseWriter, labels *metrics.Labels) boo w.WriteHeader(httpStatus) labels.RequestStatus = metricsStatus for _, err := range errs { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } rc = true } @@ -2503,32 +2005,3 @@ func checkIfAppRequest(request []byte) (bool, error) { } return false, nil } - -func (deps *endpointDeps) validateStoredBidRespAndImpExtBidders(prebid *openrtb_ext.ExtImpPrebid, storedBidResp stored_responses.ImpBidderStoredResp, impId string) error { - if storedBidResp == nil && len(prebid.StoredBidResponse) == 0 { - return nil - } - - if storedBidResp == nil { - return generateStoredBidResponseValidationError(impId) - } - if bidResponses, ok := storedBidResp[impId]; ok { - if len(bidResponses) != len(prebid.Bidder) { - return generateStoredBidResponseValidationError(impId) - } - - for bidderName := range bidResponses { - if _, bidderNameOk := deps.normalizeBidderName(bidderName); !bidderNameOk { - return fmt.Errorf(`unrecognized bidder "%v"`, bidderName) - } - if _, present := prebid.Bidder[bidderName]; !present { - return generateStoredBidResponseValidationError(impId) - } - } - } - return nil -} - -func generateStoredBidResponseValidationError(impID string) error { - return fmt.Errorf("request validation failed. Stored bid responses are specified for imp %s. Bidders specified in imp.ext should match with bidders specified in imp.ext.prebid.storedbidresponse", impID) -} diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index 835d2920af4..b4bdfbbbf21 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -149,8 +149,8 @@ func BenchmarkValidWholeExemplary(b *testing.B) { cfg := &config.Configuration{ MaxRequestSize: maxSize, - BlacklistedApps: test.Config.BlacklistedApps, - BlacklistedAppMap: test.Config.getBlacklistedAppMap(), + BlockedApps: test.Config.BlockedApps, + BlockedAppsLookup: test.Config.getBlockedAppLookup(), AccountRequired: test.Config.AccountRequired, } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index a9e16c9490b..72f5089bbf9 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -13,14 +13,14 @@ import ( "net/http/httptest" "os" "path/filepath" + "sort" "strings" "testing" "time" "github.com/buger/jsonparser" + jsoniter "github.com/json-iterator/go" "github.com/julienschmidt/httprouter" - "github.com/prebid/openrtb/v20/native1" - nativeRequests "github.com/prebid/openrtb/v20/native1/request" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/analytics" analyticsBuild "github.com/prebid/prebid-server/v2/analytics/build" @@ -33,6 +33,7 @@ import ( "github.com/prebid/prebid-server/v2/metrics" metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/v2/stored_responses" "github.com/prebid/prebid-server/v2/util/iputil" @@ -43,6 +44,11 @@ import ( const jsonFileExtension string = ".json" +func TestMain(m *testing.M) { + jsoniter.RegisterExtension(&jsonutil.RawMessageExtension{}) + os.Exit(m.Run()) +} + func TestJsonSampleRequests(t *testing.T) { testSuites := []struct { description string @@ -65,16 +71,20 @@ func TestJsonSampleRequests(t *testing.T) { "invalid-native", }, { - "Makes sure we handle (default) aliased bidders properly", + "Makes sure we handle aliased bidders properly", "aliased", }, + { + "Makes sure we handle alternate bidder codes properly", + "alternate-bidder-code", + }, { "Asserts we return 500s on requests referencing accounts with malformed configs.", "account-malformed", }, { - "Asserts we return 503s on requests with blacklisted accounts and apps.", - "blacklisted", + "Asserts we return 503s on requests with blocked apps.", + "blocked", }, { "Assert that requests that come with no user id nor app id return error if the `AccountRequired` field in the `config.Configuration` structure is set to true", @@ -159,18 +169,37 @@ func runJsonBasedTest(t *testing.T, filename, desc string) { // Build endpoint for testing. If no error, run test case cfg := &config.Configuration{MaxRequestSize: maxSize} if test.Config != nil { - cfg.BlacklistedApps = test.Config.BlacklistedApps - cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap() + cfg.BlockedApps = test.Config.BlockedApps + cfg.BlockedAppsLookup = test.Config.getBlockedAppLookup() cfg.AccountRequired = test.Config.AccountRequired } cfg.MarshalAccountDefaults() test.endpointType = OPENRTB_ENDPOINT - auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) + auctionEndpointHandler, ex, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) if assert.NoError(t, err) { assert.NotPanics(t, func() { runEndToEndTest(t, auctionEndpointHandler, test, fileData, filename) }, filename) } + if test.ExpectedValidatedBidReq != nil { + // compare as json to ignore whitespace and ext field ordering + actualJson, err := jsonutil.Marshal(ex.actualValidatedBidReq) + if assert.NoError(t, err, "Error converting actual bid request to json. Test file: %s", filename) { + assert.JSONEq(t, string(test.ExpectedValidatedBidReq), string(actualJson), "Not the expected validated request. Test file: %s", filename) + } + } + if test.ExpectedMockBidderRequests != nil { + for bidder, req := range test.ExpectedMockBidderRequests { + a, ok := ex.adapters[openrtb_ext.BidderName(bidder)] + if !ok { + t.Fatalf("Unexpected bidder %s has an expected mock bidder request. Test file: %s", bidder, filename) + } + aa := a.(*exchange.BidderAdapter) + ma := aa.Bidder.(*mockAdapter) + assert.JSONEq(t, string(req), string(ma.requestData[0]), "Not the expected mock bidder request for bidder %s. Test file: %s", bidder, filename) + } + } + // Close servers regardless if the test case was run or not for _, mockBidServer := range mockBidServers { mockBidServer.Close() @@ -443,7 +472,7 @@ func TestExplicitUserId(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, ex, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, @@ -501,7 +530,7 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(bidderMap, disabledBidders, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -556,32 +585,7 @@ func TestNilExchange(t *testing.T) { _, err := NewEndpoint( fakeUUIDGenerator{}, nil, - mockBidderParamValidator{}, - empty_fetcher.EmptyFetcher{}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - &metricsConfig.NilMetricsEngine{}, - analyticsBuild.New(&config.Analytics{}), map[string]string{}, - []byte{}, - openrtb_ext.BuildBidderMap(), - empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}, - nil, - ) - - if err == nil { - t.Errorf("NewEndpoint should return an error when given a nil Exchange.") - } -} - -// TestNilValidator makes sure we fail when given nil for the BidderParamValidator. -func TestNilValidator(t *testing.T) { - // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. - // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. - _, err := NewEndpoint( - fakeUUIDGenerator{}, - &nobidExchange{}, - nil, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -596,7 +600,7 @@ func TestNilValidator(t *testing.T) { ) if err == nil { - t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.") + t.Errorf("NewEndpoint should return an error when given a nil Exchange.") } } @@ -607,7 +611,7 @@ func TestExchangeError(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, &brokenExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -734,7 +738,7 @@ func TestImplicitIPsEndToEnd(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, exchange, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, @@ -934,7 +938,7 @@ func TestImplicitDNTEndToEnd(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, exchange, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -1170,7 +1174,7 @@ func TestStoredRequests(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -1620,7 +1624,7 @@ func TestValidateRequest(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -1644,6 +1648,8 @@ func TestValidateRequest(t *testing.T) { description string givenIsAmp bool givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error expectedChannelObject *openrtb_ext.ExtRequestPrebidChannel }{ @@ -1864,7 +1870,7 @@ func TestValidateRequest(t *testing.T) { } for _, test := range testCases { - errorList := deps.validateRequest(test.givenRequestWrapper, test.givenIsAmp, false, nil, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, test.givenIsAmp, false, nil, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) if len(errorList) == 0 { @@ -1933,9 +1939,13 @@ func TestValidateRequestExt(t *testing.T) { givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":{},"vastxml":{}}}}`), }, { - description: "prebid targeting", // test integration with validateTargeting - givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{}}}`), - expectedErrors: []string{"ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"}, + description: "prebid price granularity invalid", + givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{"pricegranularity":{"precision":-1,"ranges":[{"min":0,"max":20,"increment":0.1}]}}}}`), + expectedErrors: []string{"Price granularity error: precision must be non-negative"}, + }, + { + description: "prebid native media type price granualrity valid", + givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{"mediatypepricegranularity":{"native":{"precision":3,"ranges":[{"max":20,"increment":4.5}]}}}}}`), }, { description: "valid multibid", @@ -1976,75 +1986,9 @@ func TestValidateTargeting(t *testing.T) { givenTargeting: nil, expectedError: nil, }, - { - name: "empty", - givenTargeting: &openrtb_ext.ExtRequestTargeting{}, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners nil, includebidderkeys false", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners nil, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "includewinners false, includebidderkeys nil", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners true, includebidderkeys nil", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "all false", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners false, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "includewinners false, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: nil, - }, - { - name: "includewinners true, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, { name: "price granularity ranges out of order", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), PriceGranularity: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{ @@ -2058,7 +2002,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Video: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2073,7 +2016,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2088,7 +2030,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2103,7 +2044,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video and banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2124,7 +2064,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Video: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2139,7 +2078,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity banner incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2154,7 +2092,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2169,7 +2106,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video correct and banner incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2190,7 +2126,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native incorrect and banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2400,7 +2335,7 @@ func TestSetIntegrationType(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -2467,7 +2402,7 @@ func TestStoredRequestGenerateUuid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{id: "foo", err: nil}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -2572,7 +2507,7 @@ func TestOversizedRequest(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -2612,7 +2547,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -2651,7 +2586,7 @@ func TestNoEncoding(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, &mockExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -2736,7 +2671,7 @@ func TestContentType(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, &mockExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -2758,241 +2693,6 @@ func TestContentType(t *testing.T) { } } -func TestValidateImpExt(t *testing.T) { - type testCase struct { - description string - impExt json.RawMessage - expectedImpExt string - expectedErrs []error - } - testGroups := []struct { - description string - testCases []testCase - }{ - { - "Empty", - []testCase{ - { - description: "Empty", - impExt: nil, - expectedImpExt: "", - expectedErrs: []error{errors.New("request.imp[0].ext is required")}, - }, - }, - }, - { - "Unknown bidder tests", - []testCase{ - { - description: "Unknown Bidder only", - impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555}}`), - expectedImpExt: `{"unknownbidder":{"placement_id":555}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Unknown Prebid Ext Bidder only", - impExt: json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`), - expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Unknown Prebid Ext Bidder + First Party Data Context", - impExt: json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Unknown Bidder + First Party Data Context", - impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555} ,"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Unknown Bidder + Disabled Bidder", - impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`), - expectedImpExt: `{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Unknown Bidder + Disabled Prebid Ext Bidder", - impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`), - expectedImpExt: `{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - }, - }, - { - "Disabled bidder tests", - []testCase{ - { - description: "Disabled Bidder", - impExt: json.RawMessage(`{"disabledbidder":{"foo":"bar"}}`), - expectedImpExt: `{"disabledbidder":{"foo":"bar"}}`, - expectedErrs: []error{ - &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, - errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"), - }, - // if only bidder(s) found in request.imp[x].ext.{biddername} or request.imp[x].ext.prebid.bidder.{biddername} are disabled, return error - }, - { - description: "Disabled Prebid Ext Bidder", - impExt: json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`), - expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`, - expectedErrs: []error{ - &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, - errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"), - }, - }, - { - description: "Disabled Bidder + First Party Data Context", - impExt: json.RawMessage(`{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{ - &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, - errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"), - }, - }, - { - description: "Disabled Prebid Ext Bidder + First Party Data Context", - impExt: json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{ - &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, - errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"), - }, - }, - }, - }, - { - "First Party only", - []testCase{ - { - description: "First Party Data Context", - impExt: json.RawMessage(`{"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{ - errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"), - }, - }, - }, - }, - { - "Valid bidder tests", - []testCase{ - { - description: "Valid bidder root ext", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`, - expectedErrs: []error{}, - }, - { - description: "Valid bidder in prebid field", - impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`, - expectedErrs: []error{}, - }, - { - description: "Valid Bidder + First Party Data Context", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{}, - }, - { - description: "Valid Prebid Ext Bidder + First Party Data Context", - impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{}, - }, - { - description: "Valid Bidder + Unknown Bidder", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`), - expectedImpExt: `{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Valid Bidder + Disabled Bidder", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`, - expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, - }, - { - description: "Valid Bidder + Disabled Bidder + First Party Data Context", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, - }, - { - description: "Valid Bidder + Disabled Bidder + Unknown Bidder + First Party Data Context", - impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - { - description: "Valid Prebid Ext Bidder + Disabled Bidder Ext", - impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}}}`, - expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, - }, - { - description: "Valid Prebid Ext Bidder + Disabled Ext Bidder + First Party Data Context", - impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, - }, - { - description: "Valid Prebid Ext Bidder + Disabled Ext Bidder + Unknown Ext + First Party Data Context", - impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`), - expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, - expectedErrs: []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, - }, - }, - }, - } - - deps := &endpointDeps{ - fakeUUIDGenerator{}, - &nobidExchange{}, - mockBidderParamValidator{}, - &mockStoredReqFetcher{}, - empty_fetcher.EmptyFetcher{}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: int64(8096)}, - &metricsConfig.NilMetricsEngine{}, - analyticsBuild.New(&config.Analytics{}), - map[string]string{"disabledbidder": "The bidder 'disabledbidder' has been disabled."}, - false, - []byte{}, - openrtb_ext.BuildBidderMap(), - nil, - nil, - hardcodedResponseIPValidator{response: true}, - empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}, - nil, - openrtb_ext.NormalizeBidderName, - } - - for _, group := range testGroups { - for _, test := range group.testCases { - t.Run(test.description, func(t *testing.T) { - imp := &openrtb2.Imp{Ext: test.impExt} - impWrapper := &openrtb_ext.ImpWrapper{Imp: imp} - - errs := deps.validateImpExt(impWrapper, nil, 0, false, nil) - - assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp") - - if len(test.expectedImpExt) > 0 { - assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description) - } else { - assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description) - } - assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description) - }) - } - } -} - func validRequest(t *testing.T, filename string) string { requestData, err := os.ReadFile("sample-requests/valid-whole/supplementary/" + filename) if err != nil { @@ -3008,7 +2708,7 @@ func TestCurrencyTrunc(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3047,7 +2747,7 @@ func TestCurrencyTrunc(t *testing.T) { Cur: []string{"USD", "EUR"}, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"} assert.ElementsMatch(t, errL, []error{&expectedError}) @@ -3057,7 +2757,7 @@ func TestCCPAInvalid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3094,11 +2794,11 @@ func TestCCPAInvalid(t *testing.T) { ID: "anySiteID", }, Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"us_privacy": "invalid by length"}`), + USPrivacy: "invalid by length", }, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedWarning := errortypes.Warning{ Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", @@ -3110,7 +2810,7 @@ func TestNoSaleInvalid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3152,7 +2852,7 @@ func TestNoSaleInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided") assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3166,7 +2866,7 @@ func TestValidateSourceTID(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3204,7 +2904,7 @@ func TestValidateSourceTID(t *testing.T) { }, } - deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") } @@ -3212,7 +2912,7 @@ func TestSChainInvalid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3251,140 +2951,12 @@ func TestSChainInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.") assert.ElementsMatch(t, errL, []error{expectedError}) } -func TestMapSChains(t *testing.T) { - const seller1SChain string = `"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}` - const seller2SChain string = `"schain":{"complete":2,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":2}],"ver":"2.0"}` - - seller1SChainUnpacked := openrtb2.SupplyChain{ - Complete: 1, - Nodes: []openrtb2.SupplyChainNode{{ - ASI: "directseller1.com", - SID: "00001", - RID: "BidRequest1", - HP: openrtb2.Int8Ptr(1), - }}, - Ver: "1.0", - } - - tests := []struct { - description string - bidRequest openrtb2.BidRequest - wantReqExtSChain *openrtb2.SupplyChain - wantSourceExtSChain *openrtb2.SupplyChain - wantError bool - }{ - { - description: "invalid req.ext", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":invalid}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{}`), - }, - }, - wantError: true, - }, - { - description: "invalid source.ext", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{"schain":invalid}}`), - }, - }, - wantError: true, - }, - { - description: "req.ext.prebid.schains, req.source.ext.schain and req.ext.schain are nil", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{}`), - }, - }, - wantReqExtSChain: nil, - wantSourceExtSChain: nil, - }, - { - description: "req.ext.prebid.schains is not nil", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{}`), - }, - }, - wantReqExtSChain: nil, - wantSourceExtSChain: nil, - }, - { - description: "req.source.ext is not nil", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller1SChain + `}`), - }, - }, - wantReqExtSChain: nil, - wantSourceExtSChain: &seller1SChainUnpacked, - }, - { - description: "req.ext.schain is not nil", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{` + seller1SChain + `}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{}`), - }, - }, - wantReqExtSChain: nil, - wantSourceExtSChain: &seller1SChainUnpacked, - }, - { - description: "req.source.ext.schain and req.ext.schain are not nil", - bidRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{` + seller2SChain + `}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller1SChain + `}`), - }, - }, - wantReqExtSChain: nil, - wantSourceExtSChain: &seller1SChainUnpacked, - }, - } - - for _, test := range tests { - reqWrapper := openrtb_ext.RequestWrapper{ - BidRequest: &test.bidRequest, - } - - err := mapSChains(&reqWrapper) - - if test.wantError { - assert.NotNil(t, err, test.description) - } else { - assert.Nil(t, err, test.description) - - reqExt, err := reqWrapper.GetRequestExt() - if err != nil { - assert.Fail(t, "Error getting request ext from wrapper", test.description) - } - reqExtSChain := reqExt.GetSChain() - assert.Equal(t, test.wantReqExtSChain, reqExtSChain, test.description) - - sourceExt, err := reqWrapper.GetSourceExt() - if err != nil { - assert.Fail(t, "Error getting source ext from wrapper", test.description) - } - sourceExtSChain := sourceExt.GetSChain() - assert.Equal(t, test.wantSourceExtSChain, sourceExtSChain, test.description) - } - } -} - func TestSearchAccountID(t *testing.T) { // Correctness for lookup within Publisher object left to TestGetAccountID // This however tests the expected lookup paths in outer site, app and dooh @@ -3781,7 +3353,7 @@ func TestEidPermissionsInvalid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -3820,7 +3392,7 @@ func TestEidPermissionsInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`) assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3831,51 +3403,51 @@ func TestValidateEidPermissions(t *testing.T) { knownAliases := map[string]string{"b": "b"} testCases := []struct { - description string + name string request *openrtb_ext.ExtRequest expectedError error }{ { - description: "Valid - Empty ext", + name: "valid-empty-ext", request: &openrtb_ext.ExtRequest{}, expectedError: nil, }, { - description: "Valid - Nil ext.prebid.data", + name: "valid-nil-ext.prebid.data", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{}}, expectedError: nil, }, { - description: "Valid - Empty ext.prebid.data", + name: "valid-empty-ext.prebid.data", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{}}}, expectedError: nil, }, { - description: "Valid - Nil ext.prebid.data.eidpermissions", + name: "valid-nil-ext.prebid.data.eidpermissions", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: nil}}}, expectedError: nil, }, { - description: "Valid - None", + name: "valid-none", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{}}}}, expectedError: nil, }, { - description: "Valid - One", + name: "valid-one", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, }}}}, expectedError: nil, }, { - description: "Valid - One - Case Insensitive", + name: "valid-one-case-insensitive", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"A"}}, }}}}, expectedError: nil, }, { - description: "Valid - Many", + name: "valid-many", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Source: "sourceB", Bidders: []string{"a"}}, @@ -3883,7 +3455,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: nil, }, { - description: "Invalid - Missing Source", + name: "invalid-missing-source", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Bidders: []string{"a"}}, @@ -3891,7 +3463,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing required field: "source"`), }, { - description: "Invalid - Duplicate Source", + name: "invalid-duplicate-source", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Source: "sourceA", Bidders: []string{"a"}}, @@ -3899,7 +3471,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] duplicate entry with field: "source"`), }, { - description: "Invalid - Missing Bidders - Nil", + name: "invalid-missing-bidders-nil", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Source: "sourceB"}, @@ -3907,7 +3479,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing or empty required field: "bidders"`), }, { - description: "Invalid - Missing Bidders - Empty", + name: "invalid-missing-bidders-empty", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Source: "sourceB", Bidders: []string{}}, @@ -3915,7 +3487,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing or empty required field: "bidders"`), }, { - description: "Invalid - Invalid Bidders", + name: "invalid-invalid-bidders", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"a"}}, {Source: "sourceB", Bidders: []string{"z"}}, @@ -3923,7 +3495,7 @@ func TestValidateEidPermissions(t *testing.T) { expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] contains unrecognized bidder "z"`), }, { - description: "Valid - Alias Case Sensitive", + name: "invalid-alias-case-sensitive", request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "sourceA", Bidders: []string{"B"}}, }}}}, @@ -3933,8 +3505,10 @@ func TestValidateEidPermissions(t *testing.T) { endpoint := &endpointDeps{bidderMap: knownBidders, normalizeBidderName: fakeNormalizeBidderName} for _, test := range testCases { - result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases) - assert.Equal(t, test.expectedError, result, test.description) + t.Run(test.name, func(t *testing.T) { + result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases) + assert.Equal(t, test.expectedError, result) + }) } } @@ -4059,7 +3633,7 @@ func TestIOS14EndToEnd(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, exchange, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -4121,7 +3695,7 @@ func TestAuctionWarnings(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -4168,7 +3742,7 @@ func TestParseRequestParseImpInfoError(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -4249,7 +3823,7 @@ func TestParseGzipedRequest(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -4301,448 +3875,56 @@ func TestParseGzipedRequest(t *testing.T) { } } -func TestValidateNativeContextTypes(t *testing.T) { - impIndex := 4 - +func TestAuctionResponseHeaders(t *testing.T) { testCases := []struct { - description string - givenContextType native1.ContextType - givenSubType native1.ContextSubType - expectedError string + description string + httpRequest *http.Request + expectedStatus int + expectedHeaders func(http.Header) }{ { - description: "No Types Specified", - givenContextType: 0, - givenSubType: 0, - expectedError: "", - }, - { - description: "All Types Exchange Specific", - givenContextType: 500, - givenSubType: 500, - expectedError: "", - }, - { - description: "Context Type Known Value - Sub Type Unspecified", - givenContextType: 1, - givenSubType: 0, - expectedError: "", - }, - { - description: "Context Type Negative", - givenContextType: -1, - givenSubType: 0, - expectedError: "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Context Type Just Above Range", - givenContextType: 4, // Range is currently 1-3 - givenSubType: 0, - expectedError: "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Sub Type Negative", - givenContextType: 1, - givenSubType: -1, - expectedError: "request.imp[4].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Content - Sub Type Just Below Range", - givenContextType: 1, // Content constant - givenSubType: 9, // Content range is currently 10-15 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Content - Sub Type In Range", - givenContextType: 1, // Content constant - givenSubType: 10, // Content range is currently 10-15 - expectedError: "", - }, - { - description: "Content - Sub Type In Range - Context Type Exchange Specific Boundary", - givenContextType: 500, - givenSubType: 10, // Content range is currently 10-15 - expectedError: "", - }, - { - description: "Content - Sub Type In Range - Context Type Exchange Specific Boundary + 1", - givenContextType: 501, - givenSubType: 10, // Content range is currently 10-15 - expectedError: "", - }, - { - description: "Content - Sub Type Just Above Range", - givenContextType: 1, // Content constant - givenSubType: 16, // Content range is currently 10-15 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Content - Sub Type Exchange Specific Boundary", - givenContextType: 1, // Content constant - givenSubType: 500, - expectedError: "", - }, - { - description: "Content - Sub Type Exchange Specific Boundary + 1", - givenContextType: 1, // Content constant - givenSubType: 501, - expectedError: "", - }, - { - description: "Content - Invalid Context Type", - givenContextType: 2, // Not content constant - givenSubType: 10, // Content range is currently 10-15 - expectedError: "request.imp[4].native.request.context is 2, but contextsubtype is 10. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Social - Sub Type Just Below Range", - givenContextType: 2, // Social constant - givenSubType: 19, // Social range is currently 20-22 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Social - Sub Type In Range", - givenContextType: 2, // Social constant - givenSubType: 20, // Social range is currently 20-22 - expectedError: "", - }, - { - description: "Social - Sub Type In Range - Context Type Exchange Specific Boundary", - givenContextType: 500, - givenSubType: 20, // Social range is currently 20-22 - expectedError: "", - }, - { - description: "Social - Sub Type In Range - Context Type Exchange Specific Boundary + 1", - givenContextType: 501, - givenSubType: 20, // Social range is currently 20-22 - expectedError: "", - }, - { - description: "Social - Sub Type Just Above Range", - givenContextType: 2, // Social constant - givenSubType: 23, // Social range is currently 20-22 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", + description: "Success Response", + httpRequest: httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))), + expectedStatus: 200, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Content-Type", "application/json") + }, }, { - description: "Social - Sub Type Exchange Specific Boundary", - givenContextType: 2, // Social constant - givenSubType: 500, - expectedError: "", + description: "Failure Response", + httpRequest: httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}")), + expectedStatus: 400, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + }, }, { - description: "Social - Sub Type Exchange Specific Boundary + 1", - givenContextType: 2, // Social constant - givenSubType: 501, - expectedError: "", + description: "Success Response with Chrome BrowsingTopicsHeader", + httpRequest: func() *http.Request { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) + httpReq.Header.Add(secBrowsingTopics, "sample-value") + return httpReq + }(), + expectedStatus: 200, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Content-Type", "application/json") + h.Set("Observe-Browsing-Topics", "?1") + }, }, { - description: "Social - Invalid Context Type", - givenContextType: 3, // Not social constant - givenSubType: 20, // Social range is currently 20-22 - expectedError: "request.imp[4].native.request.context is 3, but contextsubtype is 20. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Product - Sub Type Just Below Range", - givenContextType: 3, // Product constant - givenSubType: 29, // Product range is currently 30-32 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Product - Sub Type In Range", - givenContextType: 3, // Product constant - givenSubType: 30, // Product range is currently 30-32 - expectedError: "", - }, - { - description: "Product - Sub Type In Range - Context Type Exchange Specific Boundary", - givenContextType: 500, - givenSubType: 30, // Product range is currently 30-32 - expectedError: "", - }, - { - description: "Product - Sub Type In Range - Context Type Exchange Specific Boundary + 1", - givenContextType: 501, - givenSubType: 30, // Product range is currently 30-32 - expectedError: "", - }, - { - description: "Product - Sub Type Just Above Range", - givenContextType: 3, // Product constant - givenSubType: 33, // Product range is currently 30-32 - expectedError: "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - { - description: "Product - Sub Type Exchange Specific Boundary", - givenContextType: 3, // Product constant - givenSubType: 500, - expectedError: "", - }, - { - description: "Product - Sub Type Exchange Specific Boundary + 1", - givenContextType: 3, // Product constant - givenSubType: 501, - expectedError: "", - }, - { - description: "Product - Invalid Context Type", - givenContextType: 1, // Not product constant - givenSubType: 30, // Product range is currently 30-32 - expectedError: "request.imp[4].native.request.context is 1, but contextsubtype is 30. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", - }, - } - - for _, test := range testCases { - err := validateNativeContextTypes(test.givenContextType, test.givenSubType, impIndex) - if test.expectedError == "" { - assert.NoError(t, err, test.description) - } else { - assert.EqualError(t, err, test.expectedError, test.description) - } - } -} - -func TestValidateNativePlacementType(t *testing.T) { - impIndex := 4 - - testCases := []struct { - description string - givenPlacementType native1.PlacementType - expectedError string - }{ - { - description: "Not Specified", - givenPlacementType: 0, - expectedError: "", - }, - { - description: "Known Value", - givenPlacementType: 1, // Range is currently 1-4 - expectedError: "", - }, - { - description: "Exchange Specific - Boundary", - givenPlacementType: 500, - expectedError: "", - }, - { - description: "Exchange Specific - Boundary + 1", - givenPlacementType: 501, - expectedError: "", - }, - { - description: "Negative", - givenPlacementType: -1, - expectedError: "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", - }, - { - description: "Just Above Range", - givenPlacementType: 5, // Range is currently 1-4 - expectedError: "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", - }, - } - - for _, test := range testCases { - err := validateNativePlacementType(test.givenPlacementType, impIndex) - if test.expectedError == "" { - assert.NoError(t, err, test.description) - } else { - assert.EqualError(t, err, test.expectedError, test.description) - } - } -} - -func TestValidateNativeEventTracker(t *testing.T) { - impIndex := 4 - eventIndex := 8 - - testCases := []struct { - description string - givenEvent nativeRequests.EventTracker - expectedError string - }{ - { - description: "Valid", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{1}, - }, - expectedError: "", - }, - { - description: "Event - Exchange Specific - Boundary", - givenEvent: nativeRequests.EventTracker{ - Event: 500, - Methods: []native1.EventTrackingMethod{1}, - }, - expectedError: "", - }, - { - description: "Event - Exchange Specific - Boundary + 1", - givenEvent: nativeRequests.EventTracker{ - Event: 501, - Methods: []native1.EventTrackingMethod{1}, - }, - expectedError: "", - }, - { - description: "Event - Negative", - givenEvent: nativeRequests.EventTracker{ - Event: -1, - Methods: []native1.EventTrackingMethod{1}, - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - { - description: "Event - Just Above Range", - givenEvent: nativeRequests.EventTracker{ - Event: 5, // Range is currently 1-4 - Methods: []native1.EventTrackingMethod{1}, - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - { - description: "Methods - Many Valid", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{1, 2}, - }, - expectedError: "", - }, - { - description: "Methods - Empty", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{}, - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - { - description: "Methods - Exchange Specific - Boundary", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{500}, - }, - expectedError: "", - }, - { - description: "Methods - Exchange Specific - Boundary + 1", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{501}, - }, - expectedError: "", - }, - { - description: "Methods - Negative", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{-1}, - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - { - description: "Methods - Just Above Range", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{3}, // Known values are currently 1-2 - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - { - description: "Methods - Mixed Valid + Invalid", - givenEvent: nativeRequests.EventTracker{ - Event: 1, - Methods: []native1.EventTrackingMethod{1, -1}, - }, - expectedError: "request.imp[4].native.request.eventtrackers[8].methods[1] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", - }, - } - - for _, test := range testCases { - err := validateNativeEventTracker(test.givenEvent, impIndex, eventIndex) - if test.expectedError == "" { - assert.NoError(t, err, test.description) - } else { - assert.EqualError(t, err, test.expectedError, test.description) - } - } -} - -func TestValidateNativeAssetData(t *testing.T) { - impIndex := 4 - assetIndex := 8 - - testCases := []struct { - description string - givenData nativeRequests.Data - expectedError string - }{ - { - description: "Valid", - givenData: nativeRequests.Data{Type: 1}, - expectedError: "", - }, - { - description: "Exchange Specific - Boundary", - givenData: nativeRequests.Data{Type: 500}, - expectedError: "", - }, - { - description: "Exchange Specific - Boundary + 1", - givenData: nativeRequests.Data{Type: 501}, - expectedError: "", - }, - { - description: "Not Specified", - givenData: nativeRequests.Data{}, - expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", - }, - { - description: "Negative", - givenData: nativeRequests.Data{Type: -1}, - expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", - }, - { - description: "Just Above Range", - givenData: nativeRequests.Data{Type: 13}, // Range is currently 1-12 - expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", - }, - } - - for _, test := range testCases { - err := validateNativeAssetData(&test.givenData, impIndex, assetIndex) - if test.expectedError == "" { - assert.NoError(t, err, test.description) - } else { - assert.EqualError(t, err, test.expectedError, test.description) - } - } -} - -func TestAuctionResponseHeaders(t *testing.T) { - testCases := []struct { - description string - requestBody string - expectedStatus int - expectedHeaders func(http.Header) - }{ - { - description: "Success Response", - requestBody: validRequest(t, "site.json"), - expectedStatus: 200, - expectedHeaders: func(h http.Header) { - h.Set("X-Prebid", "pbs-go/unknown") - h.Set("Content-Type", "application/json") - }, - }, - { - description: "Failure Response", - requestBody: "{}", - expectedStatus: 400, - expectedHeaders: func(h http.Header) { - h.Set("X-Prebid", "pbs-go/unknown") - }, + description: "Failure Response with Chrome BrowsingTopicsHeader", + httpRequest: func() *http.Request { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}")) + httpReq.Header.Add(secBrowsingTopics, "sample-value") + return httpReq + }(), + expectedStatus: 400, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Observe-Browsing-Topics", "?1") + }, }, } @@ -4750,7 +3932,7 @@ func TestAuctionResponseHeaders(t *testing.T) { endpoint, _ := NewEndpoint( fakeUUIDGenerator{}, exchange, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, @@ -4765,10 +3947,9 @@ func TestAuctionResponseHeaders(t *testing.T) { ) for _, test := range testCases { - httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(test.requestBody)) recorder := httptest.NewRecorder() - endpoint(recorder, httpReq, nil) + endpoint(recorder, test.httpRequest, nil) expectedHeaders := http.Header{} test.expectedHeaders(expectedHeaders) @@ -4782,38 +3963,6 @@ func TestAuctionResponseHeaders(t *testing.T) { // Test stored request data -func TestValidateBanner(t *testing.T) { - impIndex := 0 - - testCases := []struct { - description string - banner *openrtb2.Banner - impIndex int - isInterstitial bool - expectedError error - }{ - { - description: "isInterstitial Equals False (not set to 1)", - banner: &openrtb2.Banner{W: nil, H: nil, Format: nil}, - impIndex: impIndex, - isInterstitial: false, - expectedError: errors.New("request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements."), - }, - { - description: "isInterstitial Equals True (is set to 1)", - banner: &openrtb2.Banner{W: nil, H: nil, Format: nil}, - impIndex: impIndex, - isInterstitial: true, - expectedError: nil, - }, - } - - for _, test := range testCases { - result := validateBanner(test.banner, test.impIndex, test.isInterstitial) - assert.Equal(t, test.expectedError, result, test.description) - } -} - func TestParseRequestMergeBidderParams(t *testing.T) { tests := []struct { name string @@ -4850,7 +3999,7 @@ func TestParseRequestMergeBidderParams(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -4954,7 +4103,7 @@ func TestParseRequestStoredResponses(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -5067,7 +4216,7 @@ func TestParseRequestStoredBidResponses(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -5105,7 +4254,7 @@ func TestValidateStoredResp(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &nobidExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -5128,6 +4277,8 @@ func TestValidateStoredResp(t *testing.T) { testCases := []struct { description string givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error hasStoredAuctionResponses bool storedBidResponses stored_responses.ImpBidderStoredResp @@ -5669,7 +4820,7 @@ func TestValidateStoredResp(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - errorList := deps.validateRequest(test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) }) } @@ -5921,7 +5072,7 @@ func TestParseRequestMultiBid(t *testing.T) { deps := &endpointDeps{ fakeUUIDGenerator{}, &warningsCheckExchange{}, - mockBidderParamValidator{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -6108,3 +5259,726 @@ func TestValidateAliases(t *testing.T) { func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) { return openrtb_ext.BidderName(strings.ToLower(name)), true } + +func TestValidateOrFillCookieDeprecation(t *testing.T) { + type args struct { + httpReq *http.Request + req *openrtb_ext.RequestWrapper + account config.Account + } + tests := []struct { + name string + args args + wantDeviceExt json.RawMessage + wantErr error + }{ + { + name: "account-nil", + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-not-enabled", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-disabled-explicitly", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-enabled-header-not-present-in-request", + args: args{ + httpReq: &http.Request{}, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "header-present-request-device-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: nil, + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-not-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1","foo":"bar"}`), + wantErr: nil, + }, + { + name: "header-present-with-length-more-than-100", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar"}`), + wantErr: &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + { + name: "header-present-request-device-ext-cdep-present", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-invalid", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{`), + wantErr: &errortypes.FailedToUnmarshal{ + Message: "expects \" or n, but found \x00", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateOrFillCookieDeprecation(tt.args.httpReq, tt.args.req, &tt.args.account) + assert.Equal(t, tt.wantErr, err) + if tt.args.req != nil { + err := tt.args.req.RebuildRequest() + assert.NoError(t, err) + } + if tt.wantDeviceExt == nil { + if tt.args.req != nil && tt.args.req.Device != nil { + assert.Nil(t, tt.args.req.Device.Ext) + } + } else { + assert.Equal(t, string(tt.wantDeviceExt), string(tt.args.req.Device.Ext)) + } + }) + } +} + +func TestSetGPCImplicitly(t *testing.T) { + testCases := []struct { + description string + header string + regs *openrtb2.Regs + expectError bool + expectedRegs *openrtb2.Regs + }{ + { + description: "regs_ext_gpc_not_set_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "sec_gpc_header_not_set_gpc_should_not_be_modified", + header: "", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "sec_gpc_header_set_to_2_gpc_should_not_be_modified", + header: "2", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "sec_gpc_header_set_to_1_and_regs_ext_contains_other_data", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"some_other_field":"some_value"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"some_other_field":"some_value","gpc":"1"}`), + }, + }, + { + description: "regs_ext_gpc_not_set_and_header_not_set", + header: "", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "regs_ext_gpc_not_set_and_header_not_1", + header: "0", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "regs_ext_gpc_is_1_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_ext_gpc_is_1_and_header_not_1", + header: "0", + regs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_ext_other_data_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"other":"value"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"other":"value","gpc":"1"}`), + }, + }, + { + description: "regs_nil_and_header_is_1", + header: "1", + regs: nil, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_nil_and_header_not_set", + header: "", + regs: nil, + expectError: false, + expectedRegs: nil, + }, + { + description: "regs_ext_is_nil_and_header_not_set", + header: "", + regs: &openrtb2.Regs{ + Ext: nil, + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: nil, + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + httpReq := &http.Request{ + Header: http.Header{ + http.CanonicalHeaderKey("Sec-GPC"): []string{test.header}, + }, + } + + r := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: test.regs, + }, + } + + err := setGPCImplicitly(httpReq, r) + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.NoError(t, r.RebuildRequest()) + if test.expectedRegs == nil { + assert.Nil(t, r.BidRequest.Regs) + } else if test.expectedRegs.Ext == nil { + assert.Nil(t, r.BidRequest.Regs.Ext) + } else { + assert.JSONEq(t, string(test.expectedRegs.Ext), string(r.BidRequest.Regs.Ext)) + } + }) + } +} + +func TestValidateRequestCookieDeprecation(t *testing.T) { + testCases := + []struct { + name string + givenAccount *config.Account + httpReq *http.Request + reqWrapper *openrtb_ext.RequestWrapper + wantErrs []error + wantCDep string + }{ + { + name: "header-with-length-less-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "sample-value") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{}, + wantCDep: "sample-value", + }, + { + name: "header-with-length-more-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{ + &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + wantCDep: "", + }, + } + + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &warningsCheckExchange{}, + ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &mockAccountFetcher{}, + &config.Configuration{}, + &metricsConfig.NilMetricsEngine{}, + analyticsBuild.New(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + openrtb_ext.NormalizeBidderName, + } + + for _, test := range testCases { + errs := deps.validateRequest(test.givenAccount, test.httpReq, test.reqWrapper, false, false, stored_responses.ImpBidderStoredResp{}, false) + assert.Equal(t, test.wantErrs, errs) + test.reqWrapper.RebuildRequest() + deviceExt, err := test.reqWrapper.GetDeviceExt() + assert.NoError(t, err) + assert.Equal(t, test.wantCDep, deviceExt.GetCDep()) + } +} + +func TestSetSecBrowsingTopicsImplicitly(t *testing.T) { + type args struct { + httpReq *http.Request + r *openrtb_ext.RequestWrapper + account *config.Account + } + tests := []struct { + name string + args args + wantUser *openrtb2.User + }{ + { + name: "empty HTTP request, no change in user data", + args: args{ + httpReq: &http.Request{}, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: nil, + }, + { + name: "valid topic in request but topicsdomain not configured by host, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: ""}}}, + }, + wantUser: nil, + }, + { + name: "valid topic in request and topicsdomain configured by host, topics copied to user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + }, + { + name: "valid empty topic in request, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"();p=P0000000000000000000000000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: nil, + }, + { + name: "request with a few valid topics (including duplicate topics, segIDs, matching segtax, segclass, etc) and a few invalid topics(different invalid format), only valid and unique topics copied/merged to/with user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, (1 2);v=chrome.1:1:2,(4);v=chrome.1:1:2,();p=P0000000000,(4);v=chrome.1, 5);v=chrome.1, (6;v=chrome.1, ();v=chrome.1, ( );v=chrome.1, (1);v=chrome.1:1:2, (1 2 4 6 7 4567 ) ; v=chrome.1: 2 : 3,();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + }, + Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + }}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + }, + Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "4"}, + {ID: "6"}, + {ID: "7"}, + {ID: "4567"}, + }, + Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setSecBrowsingTopicsImplicitly(tt.args.httpReq, tt.args.r, tt.args.account) + + // sequence is not guaranteed we're using a map to filter segids + sortUserData(tt.wantUser) + sortUserData(tt.args.r.User) + assert.Equal(t, tt.wantUser, tt.args.r.User, tt.name) + }) + } +} + +func sortUserData(user *openrtb2.User) { + if user != nil { + sort.Slice(user.Data, func(i, j int) bool { + if user.Data[i].Name == user.Data[j].Name { + return string(user.Data[i].Ext) < string(user.Data[j].Ext) + } + return user.Data[i].Name < user.Data[j].Name + }) + for g := range user.Data { + sort.Slice(user.Data[g].Segment, func(i, j int) bool { + return user.Data[g].Segment[i].ID < user.Data[g].Segment[j].ID + }) + } + } +} diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json index 8dc19f6f24d..30d8b9ca240 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json @@ -74,9 +74,7 @@ } ], "regs": { - "ext": { - "us_privacy": "1YYY" - } + "us_privacy": "1YYY" }, "ext": { "prebid": { diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json index 4003abf99cb..8c232192a63 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json @@ -74,14 +74,10 @@ } ], "regs": { - "ext": { - "gdpr": 1 - } + "gdpr": 1 }, "user": { - "ext": { - "consent": "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" - } + "consent": "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" }, "ext": { "prebid": { diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json index c6389dadc29..0249bbc3f96 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json @@ -79,9 +79,7 @@ } ], "regs": { - "ext": { - "gdpr": 1 - } + "gdpr": 1 }, "ext": { "prebid": { diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json index b62a745b1bf..6f0f5780b43 100644 --- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json +++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json @@ -74,14 +74,10 @@ } ], "regs": { - "ext": { - "gdpr": 1 - } + "gdpr": 1 }, "user": { - "ext": { - "consent": "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" - } + "consent": "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" }, "ext": { "prebid": { diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/ortb-2.5-to-2.6-upconvert.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/ortb-2.5-to-2.6-upconvert.json new file mode 100644 index 00000000000..766b7fc6ba9 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/ortb-2.5-to-2.6-upconvert.json @@ -0,0 +1,306 @@ +{ + "description": "Amp request with all 2.5 ext fields that were moved into 2.6 ortb fields", + "query": "tag_id=101", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 15 + } + ] + }, + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + } + }, + "is_rewarded_inventory": 1 + } + } + } + ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + } + }, + "expectedValidatedBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "ext": { + "amp": 1 + } + }, + "device": { + "ip": "192.0.2.1" + }, + "at": 1, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + } + } + } + }, + "secure": 1, + "rwdd": 1 + } + ], + "regs": { + "gdpr": 1, + "us_privacy": "1YYY" + }, + "user": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + }, + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + }, + "ext": { + "prebid": { + "cache": { + "bids": {} + }, + "channel": { + "name": "amp", + "version": "" + }, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true, + "mediatypepricegranularity": {} + } + } + } + }, + "expectedMockBidderRequests": { + "appnexus": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "ext": { + "amp": 1 + } + }, + "device": { + "ip": "192.0.2.1" + }, + "at": 1, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 12883451 + }, + "prebid": { + "is_rewarded_inventory": 1 + } + }, + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "ext": { + "prebid": { + "channel": { + "name": "amp", + "version": "" + } + } + } + } + }, + "expectedAmpResponse": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_id": "0", + "hb_cache_id_appnexus": "0", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "15.00", + "hb_pb_appnexus": "15.00" + }, + "ortb2": { + "ext": { + "warnings": { + "general": [ + { + "code": 10002, + "message": "debug turned off for account" + } + ] + } + } + } + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json b/endpoints/openrtb2/sample-requests/blocked/blocked-app.json similarity index 96% rename from endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json rename to endpoints/openrtb2/sample-requests/blocked/blocked-app.json index 120fcec08f4..88e0ed43496 100644 --- a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json +++ b/endpoints/openrtb2/sample-requests/blocked/blocked-app.json @@ -1,7 +1,7 @@ { - "description": "This is a perfectly valid request except that it comes from a blacklisted App", + "description": "This is a perfectly valid request except that it comes from a blocked app", "config": { - "blacklistedApps": ["spam_app"] + "blockedApps": ["spam_app"] }, "mockBidRequest": { "id": "some-request-id", diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json deleted file mode 100644 index dc15410a290..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "description": "Invalid GDPR value in regs field", - "mockBidRequest": { - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } - ] - } - } - ], - "regs": { - "ext": { - "gdpr": "foo" - } - }, - "user": { - "ext": {} - } - }, - "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: gdpr must be an integer\n" -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json index 7ab2631b701..4a513f703b1 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json @@ -42,5 +42,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: expect { or n, but found " + "expectedErrorMessage": "Invalid request: req.regs.ext is invalid: expect { or n, but found " } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-gdpr-invalid.json similarity index 86% rename from endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json rename to endpoints/openrtb2/sample-requests/invalid-whole/regs-gdpr-invalid.json index 03e789eef86..40b7281d572 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-gdpr-invalid.json @@ -35,14 +35,12 @@ } ], "regs": { - "ext": { - "gdpr": 2 - } + "gdpr": 2 }, "user": { "ext": {} } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1\n" + "expectedErrorMessage": "Invalid request: request.regs.gdpr must be either 0 or 1\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-source-empty.json similarity index 63% rename from endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json rename to endpoints/openrtb2/sample-requests/invalid-whole/user-eids-source-empty.json index 910e9650d75..902a2d9c1b6 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-source-empty.json @@ -1,5 +1,5 @@ { - "description": "Bid request where a request.user.ext.eids.uids array element is missing its id field", + "description": "Bid request with user.eids array element that does not contain source field", "mockBidRequest": { "id": "anyRequestID", "site": { @@ -29,14 +29,13 @@ }], "tmax": 1000, "user": { - "ext": { - "eids": [{ - "source": "source1", - "uids": [{}] + "eids": [{ + "uids": [{ + "id": "A" }] - } + }] } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext.eids[0].uids[0] missing required field: \"id\"\n" + "expectedErrorMessage": "Invalid request: request.user.eids[0] missing required field: \"source\"\n" } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-id-empty.json similarity index 62% rename from endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json rename to endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-id-empty.json index 3a451ecbd76..c8eb07aa335 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-id-empty.json @@ -1,5 +1,5 @@ { - "description": "Bid request with user.ext.eids array element array element that does not contain source field", + "description": "Bid request where a request.user.eids.uids array element is missing its id field", "mockBidRequest": { "id": "anyRequestID", "site": { @@ -29,15 +29,12 @@ }], "tmax": 1000, "user": { - "ext": { - "eids": [{ - "uids": [{ - "id": "A" - }] - }] - } + "eids": [{ + "source": "source1", + "uids": [{}] + }] } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext.eids[0] missing required field: \"source\"\n" + "expectedErrorMessage": "Invalid request: request.user.eids[0].uids[0] missing required field: \"id\"\n" } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-missing.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-missing.json similarity index 70% rename from endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-missing.json rename to endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-missing.json index eed386b4c7d..3e55c33b849 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-missing.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-eids-uids-missing.json @@ -1,5 +1,5 @@ { - "description": "Bid request with user.ext.eids array element array element that does not contain uids", + "description": "Bid request with user.eids array element array element that does not contain uids", "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { @@ -37,13 +37,11 @@ } }, "user": { - "ext": { - "eids": [{ - "source": "source1" - }] - } + "eids": [{ + "source": "source1" + }] } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext.eids[0].uids must contain at least one element or be undefined\n" + "expectedErrorMessage": "Invalid request: request.user.eids[0].uids must contain at least one element or be undefined\n" } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json index af04627c3a9..222ffb993b7 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json @@ -46,5 +46,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: expects \" or n, but found 1" + "expectedErrorMessage": "Invalid request: req.user.ext is invalid: expects \" or n, but found 1" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json index b710d589ea5..ae9d72c8682 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json @@ -41,5 +41,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: expects \" or n, but found 2" + "expectedErrorMessage": "Invalid request: req.user.ext is invalid: expects \" or n, but found 2" } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/device-sua.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/device-sua.json new file mode 100644 index 00000000000..0f85f904166 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/device-sua.json @@ -0,0 +1,100 @@ +{ + "description": "Bid request defines an valid request.device.sua value", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 0.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "ext": { + "appnexus": { + "placementId": 12883451 + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } + } + ], + "device": { + "ua": "Mozilla", + "geo": { + "lat": 123.456, + "lon": 678.90, + "zip": "90210" + }, + "sua": { + "browsers": [ + { + "brand": "MS", + "ext": {} + }, + { + "brand": "MS", + "ext": {} + } + ], + "platform": { + "brand": "MS", + "ext": {} + }, + "model": "mac" + }, + "dnt": 1, + "lmt": 1 + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 0, + "ext": { + "origbidcpm": 0, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.5-to-2.6-upconvert.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.5-to-2.6-upconvert.json new file mode 100644 index 00000000000..3ff235c4b30 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.5-to-2.6-upconvert.json @@ -0,0 +1,393 @@ +{ + "description": "Request with all 2.5 ext fields that were moved into 2.6 ortb fields", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 15 + }, + { + "bidderName": "rubicon", + "currency": "USD", + "price": 1.00 + } + ], + "bidderInfoOverrides": { + "appnexus": { + "openrtb": { + "version": "2.5" + } + }, + "rubicon": { + "openrtb": { + "version": "2.6" + } + } + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + }, + "is_rewarded_inventory": 1 + } + } + } + ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "ext": {} + }, + "expectedValidatedBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + }, + "secure": 1, + "rwdd": 1 + } + ], + "regs": { + "gdpr": 1, + "us_privacy": "1YYY" + }, + "user": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + }, + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "expectedMockBidderRequests": { + "appnexus": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 12883451 + }, + "prebid": { + "is_rewarded_inventory": 1 + } + }, + "secure": 1 + }], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + } + }, + "rubicon": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + }, + "secure": 1, + "rwdd": 1 + }], + "regs": { + "gdpr": 1, + "us_privacy": "1YYY" + }, + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + }, + "user": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 15, + "ext": { + "origbidcpm": 15, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + }, + { + "bid": [ + { + "id": "rubicon-bid", + "impid": "some-impression-id", + "price": 1.00, + "ext": { + "origbidcpm": 1.00, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "rubicon" + }, + "type": "banner" + } + } + } + ], + "seat": "rubicon" + } + ], + "bidid": "test-bid-id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.6-to-2.5-downconvert.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.6-to-2.5-downconvert.json new file mode 100644 index 00000000000..28ba8a707e0 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/ortb-2.6-to-2.5-downconvert.json @@ -0,0 +1,417 @@ +{ + "description": "Request with all 2.5 ext fields that were moved into 2.6 ortb fields", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 15 + }, + { + "bidderName": "rubicon", + "currency": "USD", + "price": 1.00 + } + ], + "bidderInfoOverrides": { + "appnexus": { + "openrtb": { + "version": "2.5" + } + }, + "rubicon": { + "openrtb": { + "version": "2.6" + } + } + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "inventorypartnerdomain": "any-domain" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + }, + "is_rewarded_inventory": 1 + } + }, + "refresh": { + "count": 10 + } + } + ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "ext": {}, + "cattax": 20, + "acat": ["any-acat"] + }, + "expectedValidatedBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "inventorypartnerdomain": "any-domain", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + }, + "secure": 1, + "rwdd": 1, + "refresh": { + "count": 10 + } + } + ], + "regs": { + "gdpr": 1, + "us_privacy": "1YYY" + }, + "user": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + }, + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + }, + "cattax": 20, + "acat": ["any-acat"] + }, + "expectedMockBidderRequests": { + "appnexus": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "inventorypartnerdomain": "any-domain", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 12883451 + }, + "prebid": { + "is_rewarded_inventory": 1 + } + }, + "refresh": { + "count": 10 + }, + "secure": 1 + }], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + } + }, + "user": { + "ext": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "cattax": 20, + "acat": ["any-acat"] + }, + "rubicon": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "inventorypartnerdomain": "any-domain", + "ext": { + "amp": 0 + } + }, + "at": 1, + "device": { + "ip": "192.0.2.1" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + }, + "secure": 1, + "refresh": { + "count": 10 + }, + "rwdd": 1 + }], + "regs": { + "gdpr": 1, + "us_privacy": "1YYY" + }, + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + } + ], + "ver": "2.0" + } + }, + "user": { + "consent": "some-consent-string", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + }, + "cattax": 20, + "acat": ["any-acat"] + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 15, + "ext": { + "origbidcpm": 15, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + }, + { + "bid": [ + { + "id": "rubicon-bid", + "impid": "some-impression-id", + "price": 1.00, + "ext": { + "origbidcpm": 1.00, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "rubicon" + }, + "type": "banner" + } + } + } + ], + "seat": "rubicon" + } + ], + "bidid": "test-bid-id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/source-schain.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/source-schain.json new file mode 100644 index 00000000000..c25f7ca1c47 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/source-schain.json @@ -0,0 +1,91 @@ +{ + "description": "Bid request defines a valid request.source.schain.nodes value", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 0.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "ext": { + "appnexus": { + "placementId": 12883451 + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } + } + ], + "source": { + "fd": 1, + "tid": "abc123", + "pchain": "tag_placement", + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "asi", + "sid": "sid", + "rid": "rid", + "ext": {} + } + ], + "ver": "ver", + "ext": {} + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 0, + "ext": { + "origbidcpm": 0, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict.json index 3d6a0774b9d..9565e41af1f 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict.json @@ -37,10 +37,7 @@ "regs": { "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN", "gpp_sid": [6], - "gdpr": 1, - "ext": { - "us_privacy": "1YYY" - } + "gdpr": 1 }, "user": { "consent": "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA", diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict2.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict2.json index 2102a8cd44b..54477ed0986 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict2.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-conflict2.json @@ -37,10 +37,7 @@ "regs": { "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN", "gpp_sid": [2,6], - "gdpr": 1, - "ext": { - "us_privacy": "1YYY" - } + "gdpr": 1 }, "user": { "consent": "Invalid", diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json index 2ccdfb7ccdc..df2c426d0c3 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json @@ -35,9 +35,7 @@ } ], "regs": { - "ext": { - "us_privacy": "{invalid}" - } + "us_privacy": "{invalid}" }, "user": { "ext": {} diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go index 6fedb73cd47..329f7952369 100644 --- a/endpoints/openrtb2/test_utils.go +++ b/endpoints/openrtb2/test_utils.go @@ -34,6 +34,7 @@ import ( "github.com/prebid/prebid-server/v2/metrics" metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" pbc "github.com/prebid/prebid-server/v2/prebid_cache_client" "github.com/prebid/prebid-server/v2/stored_requests" "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" @@ -63,15 +64,16 @@ const ( type testCase struct { // Common - endpointType int - Description string `json:"description"` - Config *testConfigValues `json:"config"` - BidRequest json.RawMessage `json:"mockBidRequest"` - ExpectedValidatedBidReq json.RawMessage `json:"expectedValidatedBidRequest"` - ExpectedReturnCode int `json:"expectedReturnCode,omitempty"` - ExpectedErrorMessage string `json:"expectedErrorMessage"` - Query string `json:"query"` - planBuilder hooks.ExecutionPlanBuilder + endpointType int + Description string `json:"description"` + Config *testConfigValues `json:"config"` + BidRequest json.RawMessage `json:"mockBidRequest"` + ExpectedValidatedBidReq json.RawMessage `json:"expectedValidatedBidRequest"` + ExpectedMockBidderRequests map[string]json.RawMessage `json:"expectedMockBidderRequests"` + ExpectedReturnCode int `json:"expectedReturnCode,omitempty"` + ExpectedErrorMessage string `json:"expectedErrorMessage"` + Query string `json:"query"` + planBuilder hooks.ExecutionPlanBuilder // "/openrtb2/auction" endpoint JSON test info ExpectedBidResponse json.RawMessage `json:"expectedBidResponse"` @@ -83,13 +85,20 @@ type testCase struct { } type testConfigValues struct { - AccountRequired bool `json:"accountRequired"` - AliasJSON string `json:"aliases"` - BlacklistedApps []string `json:"blacklistedApps"` - DisabledAdapters []string `json:"disabledAdapters"` - CurrencyRates map[string]map[string]float64 `json:"currencyRates"` - MockBidders []mockBidderHandler `json:"mockBidders"` - RealParamsValidator bool `json:"realParamsValidator"` + AccountRequired bool `json:"accountRequired"` + AliasJSON string `json:"aliases"` + BlockedApps []string `json:"blockedApps"` + DisabledAdapters []string `json:"disabledAdapters"` + CurrencyRates map[string]map[string]float64 `json:"currencyRates"` + MockBidders []mockBidderHandler `json:"mockBidders"` + RealParamsValidator bool `json:"realParamsValidator"` + BidderInfos map[string]bidderInfoOverrides `json:"bidderInfoOverrides"` +} +type bidderInfoOverrides struct { + OpenRTB *OpenRTBInfo `json:"openrtb"` +} +type OpenRTBInfo struct { + Version string `json:"version"` } type brokenExchange struct{} @@ -941,6 +950,7 @@ type mockBidderHandler struct { Currency string `json:"currency"` Price float64 `json:"price"` DealID string `json:"dealid"` + Seat string `json:"seat"` } func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) { @@ -999,6 +1009,8 @@ func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) { type mockAdapter struct { mockServerURL string Server config.Server + seat string + requestData [][]byte } func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { @@ -1009,7 +1021,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co return adapter, nil } -func (a mockAdapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { +func (a *mockAdapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var requests []*adapters.RequestData var errors []error @@ -1029,11 +1041,12 @@ func (a mockAdapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *ada Body: requestJSON, } requests = append(requests, requestData) + a.requestData = append(a.requestData, requestData.Body) } return requests, errors } -func (a mockAdapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (a *mockAdapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { if responseData.StatusCode != http.StatusOK { switch responseData.StatusCode { case http.StatusNoContent: @@ -1064,6 +1077,9 @@ func (a mockAdapter) MakeBids(request *openrtb2.BidRequest, requestData *adapter Bid: &seatBid.Bid[i], BidType: openrtb_ext.BidTypeBanner, } + if len(a.seat) > 0 { + b.Seat = openrtb_ext.BidderName(a.seat) + } rv.Bids = append(rv.Bids, b) } } @@ -1091,6 +1107,24 @@ func getBidderInfos(disabledAdapters []string, biddersNames []openrtb_ext.Bidder return biddersInfos } +func enableBidders(bidderInfos config.BidderInfos) { + for name, bidderInfo := range bidderInfos { + if bidderInfo.Disabled { + bidderInfo.Disabled = false + bidderInfos[name] = bidderInfo + } + } +} + +func disableBidders(disabledAdapters []string, bidderInfos config.BidderInfos) { + for _, disabledAdapter := range disabledAdapters { + if bidderInfo, ok := bidderInfos[disabledAdapter]; ok { + bidderInfo.Disabled = true + bidderInfos[disabledAdapter] = bidderInfo + } + } +} + func newBidderInfo(isDisabled bool) config.BidderInfo { return config.BidderInfo{ Disabled: isDisabled, @@ -1135,27 +1169,40 @@ func parseTestData(fileData []byte, testFile string) (testCase, error) { return parsedTestData, fmt.Errorf("Test case %s should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.", testFile) } + // Get optional expected validated bid request + parsedTestData.ExpectedValidatedBidReq, _, _, err = jsonparser.Get(fileData, "expectedValidatedBidRequest") + + // Get optional expected mock bidder requests + jsonExpectedMockBidderRequests, _, _, err := jsonparser.Get(fileData, "expectedMockBidderRequests") + if err == nil && jsonExpectedMockBidderRequests != nil { + parsedTestData.ExpectedMockBidderRequests = make(map[string]json.RawMessage) + if err = jsonutil.UnmarshalValid(jsonExpectedMockBidderRequests, &parsedTestData.ExpectedMockBidderRequests); err != nil { + return parsedTestData, fmt.Errorf("Error unmarshaling root.expectedMockBidderRequests from file %s. Desc: %v.", testFile, err) + } + } + parsedTestData.ExpectedReturnCode = int(parsedReturnCode) return parsedTestData, nil } -func (tc *testConfigValues) getBlacklistedAppMap() map[string]bool { - var blacklistedAppMap map[string]bool +func (tc *testConfigValues) getBlockedAppLookup() map[string]bool { + var blockedAppLookup map[string]bool - if len(tc.BlacklistedApps) > 0 { - blacklistedAppMap = make(map[string]bool, len(tc.BlacklistedApps)) - for _, app := range tc.BlacklistedApps { - blacklistedAppMap[app] = true + if len(tc.BlockedApps) > 0 { + blockedAppLookup = make(map[string]bool, len(tc.BlockedApps)) + for _, app := range tc.BlockedApps { + blockedAppLookup[app] = true } } - return blacklistedAppMap + return blockedAppLookup } // exchangeTestWrapper is a wrapper that asserts the openrtb2 bid request just before the HoldAuction call type exchangeTestWrapper struct { ex exchange.Exchange actualValidatedBidReq *openrtb2.BidRequest + adapters map[openrtb_ext.BidderName]exchange.AdaptedBidder } func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { @@ -1173,16 +1220,16 @@ func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.Auct } // buildTestExchange returns an exchange with mock bidder servers and mock currency conversion server -func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher) (exchange.Exchange, []*httptest.Server) { +func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher, requestValidator ortb.RequestValidator) (exchange.Exchange, []*httptest.Server) { if len(testCfg.MockBidders) == 0 { testCfg.MockBidders = append(testCfg.MockBidders, mockBidderHandler{BidderName: "appnexus", Currency: "USD", Price: 0.00}) } for _, mockBidder := range testCfg.MockBidders { bidServer := httptest.NewServer(http.HandlerFunc(mockBidder.bid)) - bidderAdapter := mockAdapter{mockServerURL: bidServer.URL} + bidderAdapter := mockAdapter{mockServerURL: bidServer.URL, seat: mockBidder.Seat} bidderName := openrtb_ext.BidderName(mockBidder.BidderName) - adapterMap[bidderName] = exchange.AdaptBidder(bidderAdapter, bidServer.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, bidderName, nil, "") + adapterMap[bidderName] = exchange.AdaptBidder(&bidderAdapter, bidServer.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, bidderName, nil, "") mockBidServersArray = append(mockBidServersArray, bidServer) } @@ -1194,8 +1241,10 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid }.Builder testExchange := exchange.NewExchange(adapterMap, + &wellBehavedCache{}, cfg, + requestValidator, nil, met, bidderInfos, @@ -1208,7 +1257,8 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid ) testExchange = &exchangeTestWrapper{ - ex: testExchange, + ex: testExchange, + adapters: adapterMap, } return testExchange, mockBidServersArray @@ -1231,9 +1281,24 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han paramValidator = mockBidderParamValidator{} } - bidderInfos := getBidderInfos(test.Config.DisabledAdapters, openrtb_ext.CoreBidderNames()) + bidderInfos, _ := config.LoadBidderInfoFromDisk("../../static/bidder-info") + for bidder, overrides := range test.Config.BidderInfos { + if bi, ok := bidderInfos[bidder]; ok { + if overrides.OpenRTB != nil && len(overrides.OpenRTB.Version) > 0 { + if bi.OpenRTB == nil { + bi.OpenRTB = &config.OpenRTBInfo{} + } + bi.OpenRTB.Version = overrides.OpenRTB.Version + bidderInfos[bidder] = bi + } + } + } + + enableBidders(bidderInfos) + disableBidders(test.Config.DisabledAdapters, bidderInfos) bidderMap := exchange.GetActiveBidders(bidderInfos) disabledBidders := exchange.GetDisabledBidderWarningMessages(bidderInfos) + requestValidator := ortb.NewRequestValidator(bidderMap, disabledBidders, paramValidator) met := &metricsConfig.NilMetricsEngine{} mockFetcher := empty_fetcher.EmptyFetcher{} @@ -1249,7 +1314,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han } mockCurrencyRatesServer := httptest.NewServer(http.HandlerFunc(mockCurrencyConversionService.handle)) - testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher) + testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher, requestValidator) var storedRequestFetcher stored_requests.Fetcher if len(test.StoredRequest) > 0 { @@ -1267,8 +1332,9 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han accountFetcher := &mockAccountFetcher{ data: map[string]json.RawMessage{ - "malformed_acct": json.RawMessage(`{"disabled":"invalid type"}`), - "disabled_acct": json.RawMessage(`{"disabled":true}`), + "malformed_acct": json.RawMessage(`{"disabled":"invalid type"}`), + "disabled_acct": json.RawMessage(`{"disabled":true}`), + "alternate_bidder_code_acct": json.RawMessage(`{"disabled":false,"alternatebiddercodes":{"enabled":true,"bidders":{"appnexus":{"enabled":true,"allowedbiddercodes":["groupm"]}}}}`), }, } @@ -1277,7 +1343,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han planBuilder = hooks.EmptyPlanBuilder{} } - var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, openrtb_ext.BidderParamValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.Runner, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder, *exchange.TmaxAdjustmentsPreprocessed) (httprouter.Handle, error) + var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, ortb.RequestValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.Runner, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder, *exchange.TmaxAdjustmentsPreprocessed) (httprouter.Handle, error) switch test.endpointType { case AMP_ENDPOINT: @@ -1289,7 +1355,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han endpoint, err := endpointBuilder( fakeUUIDGenerator{}, testExchange, - paramValidator, + requestValidator, storedRequestFetcher, accountFetcher, cfg, @@ -1400,10 +1466,10 @@ func (p *fakePermissions) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { +func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions { return gdpr.AuctionPermissions{ AllowBidRequest: true, - }, nil + } } type mockPlanBuilder struct { diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 22248f1f36c..9625228fa82 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -46,7 +46,7 @@ var defaultRequestTimeout int64 = 5000 func NewVideoEndpoint( uuidGenerator uuidutil.UUIDGenerator, ex exchange.Exchange, - validator openrtb_ext.BidderParamValidator, + requestValidator ortb.RequestValidator, requestsById stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, accounts stored_requests.AccountFetcher, @@ -60,7 +60,7 @@ func NewVideoEndpoint( tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, ) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { + if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewVideoEndpoint requires non-nil arguments.") } @@ -76,7 +76,7 @@ func NewVideoEndpoint( return httprouter.Handle((&endpointDeps{ uuidGenerator, ex, - validator, + requestValidator, requestsById, videoFetcher, accounts, @@ -165,6 +165,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re }() w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) lr := &io.LimitedReader{ R: r.Body, @@ -259,16 +260,12 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re // all code after this line should use the bidReqWrapper instead of bidReq directly bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq} - // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(r, bidReqWrapper) - - if err := ortb.SetDefaults(bidReqWrapper); err != nil { - handleError(&labels, w, errL, &vo, &debugLog) + if err := openrtb_ext.ConvertUpTo26(bidReqWrapper); err != nil { + handleError(&labels, w, []error{err}, &vo, &debugLog) return } - errL = deps.validateRequest(bidReqWrapper, false, false, nil, false) - if errortypes.ContainsFatalError(errL) { + if err := ortb.SetDefaults(bidReqWrapper); err != nil { handleError(&labels, w, errL, &vo, &debugLog) return } @@ -306,8 +303,22 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } + // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). + if errs := deps.setFieldsImplicitly(r, bidReqWrapper, account); len(errs) > 0 { + errL = append(errL, errs...) + } + + errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) + errL = append(errL, errs...) + if errortypes.ContainsFatalError(errL) { + handleError(&labels, w, errL, &vo, &debugLog) + return + } + activityControl = privacy.NewActivityControl(&account.Privacy) + warnings := errortypes.WarningOnly(errL) + secGPC := r.Header.Get("Sec-GPC") auctionRequest := &exchange.AuctionRequest{ BidRequestWrapper: bidReqWrapper, @@ -316,6 +327,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re RequestType: labels.RType, StartTime: start, LegacyLabels: labels, + Warnings: warnings, GlobalPrivacyControlHeader: secGPC, PubID: labels.PubID, HookExecutor: hookexecution.EmptyHookExecutor{}, @@ -404,9 +416,9 @@ func handleError(labels *metrics.Labels, w http.ResponseWriter, errL []error, vo var status int = http.StatusInternalServerError for _, er := range errL { erVal := errortypes.ReadCode(er) - if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode { + if erVal == errortypes.BlockedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode { status = http.StatusServiceUnavailable - labels.RequestStatus = metrics.RequestStatusBlacklisted + labels.RequestStatus = metrics.RequestStatusBlockedApp break } else if erVal == errortypes.AcctRequiredErrorCode { status = http.StatusBadRequest @@ -496,7 +508,7 @@ func createImpressionTemplate(imp openrtb2.Imp, video *openrtb2.Video) openrtb2. } func (deps *endpointDeps) loadStoredImp(storedImpId string) (openrtb2.Imp, []error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond) defer cancel() impr := openrtb2.Imp{} @@ -806,8 +818,8 @@ func (deps *endpointDeps) validateVideoRequest(req *openrtb_ext.BidRequestVideo) errL = append(errL, err) } else if req.App != nil { if req.App.ID != "" { - if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found { - err := &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} + if _, found := deps.cfg.BlockedAppsLookup[req.App.ID]; found { + err := &errortypes.BlockedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} errL = append(errL, err) return errL, podErrors } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 98d6ca35c49..8670068cbea 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -813,15 +813,15 @@ func TestHandleError(t *testing.T) { &errortypes.AccountDisabled{}, }, wantCode: 503, - wantMetricsStatus: metrics.RequestStatusBlacklisted, + wantMetricsStatus: metrics.RequestStatusBlockedApp, }, { description: "Blocked app - return 503 with blocked metrics status", giveErrors: []error{ - &errortypes.BlacklistedApp{}, + &errortypes.BlockedApp{}, }, wantCode: 503, - wantMetricsStatus: metrics.RequestStatusBlacklisted, + wantMetricsStatus: metrics.RequestStatusBlockedApp, }, { description: "Account required error - return 400 with bad input metrics status", @@ -1090,14 +1090,11 @@ func TestCCPA(t *testing.T) { if ex.lastRequest == nil { t.Fatalf("%s: The request never made it into the exchange.", test.description) } - extRegs := &openrtb_ext.ExtRegs{} - if err := jsonutil.UnmarshalValid(ex.lastRequest.Regs.Ext, extRegs); err != nil { - t.Fatalf("%s: Failed to unmarshal reg.ext in request to the exchange: %v", test.description, err) - } + if test.expectConsentString { - assert.Len(t, extRegs.USPrivacy, 4, test.description+":consent") + assert.Len(t, ex.lastRequest.Regs.USPrivacy, 4, test.description+":consent") } else if test.expectEmptyConsent { - assert.Empty(t, extRegs.USPrivacy, test.description+":consent") + assert.Empty(t, ex.lastRequest.Regs.USPrivacy, test.description+":consent") } // Validate HTTP Response diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index d576b8a0093..87208a09114 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -1701,12 +1701,12 @@ func (g *fakePermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return false, nil } -func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { +func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions { return gdpr.AuctionPermissions{ AllowBidRequest: g.personalInfoAllowed, PassGeo: g.personalInfoAllowed, PassID: g.personalInfoAllowed, - }, nil + } } type fakeSyncer struct { diff --git a/errortypes/code.go b/errortypes/code.go index 399dd663498..357daa846d5 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -5,7 +5,7 @@ const ( UnknownErrorCode = 999 TimeoutErrorCode = iota BadInputErrorCode - BlacklistedAppErrorCode + BlockedAppErrorCode BadServerResponseErrorCode FailedToRequestBidsErrorCode BidderTemporarilyDisabledErrorCode diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index d31c4166b06..1d33fa3c9d4 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -59,23 +59,20 @@ func (err *BadInput) Severity() Severity { return SeverityFatal } -// BlacklistedApp should be used when a request App.ID matches an entry in the BlacklistedApps -// environment variable array -// -// These errors will be written to http.ResponseWriter before canceling execution -type BlacklistedApp struct { +// BlockedApp should be used when a request App.ID matches an entry in the BlockedApp configuration. +type BlockedApp struct { Message string } -func (err *BlacklistedApp) Error() string { +func (err *BlockedApp) Error() string { return err.Message } -func (err *BlacklistedApp) Code() int { - return BlacklistedAppErrorCode +func (err *BlockedApp) Code() int { + return BlockedAppErrorCode } -func (err *BlacklistedApp) Severity() Severity { +func (err *BlockedApp) Severity() Severity { return SeverityFatal } diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index e188d2936e2..7f1496810e2 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -13,6 +13,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/adkernel" "github.com/prebid/prebid-server/v2/adapters/adkernelAdn" "github.com/prebid/prebid-server/v2/adapters/adman" + "github.com/prebid/prebid-server/v2/adapters/admatic" "github.com/prebid/prebid-server/v2/adapters/admixer" "github.com/prebid/prebid-server/v2/adapters/adnuntius" "github.com/prebid/prebid-server/v2/adapters/adocean" @@ -25,6 +26,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/adsinteractive" "github.com/prebid/prebid-server/v2/adapters/adtarget" "github.com/prebid/prebid-server/v2/adapters/adtelligent" + "github.com/prebid/prebid-server/v2/adapters/adtonos" "github.com/prebid/prebid-server/v2/adapters/adtrgtme" "github.com/prebid/prebid-server/v2/adapters/advangelists" "github.com/prebid/prebid-server/v2/adapters/adview" @@ -38,6 +40,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/apacdex" "github.com/prebid/prebid-server/v2/adapters/appnexus" "github.com/prebid/prebid-server/v2/adapters/appush" + "github.com/prebid/prebid-server/v2/adapters/aso" "github.com/prebid/prebid-server/v2/adapters/audienceNetwork" "github.com/prebid/prebid-server/v2/adapters/automatad" "github.com/prebid/prebid-server/v2/adapters/avocet" @@ -49,10 +52,12 @@ import ( "github.com/prebid/prebid-server/v2/adapters/between" "github.com/prebid/prebid-server/v2/adapters/beyondmedia" "github.com/prebid/prebid-server/v2/adapters/bidmachine" + "github.com/prebid/prebid-server/v2/adapters/bidmatic" "github.com/prebid/prebid-server/v2/adapters/bidmyadz" "github.com/prebid/prebid-server/v2/adapters/bidscube" "github.com/prebid/prebid-server/v2/adapters/bidstack" - "github.com/prebid/prebid-server/v2/adapters/bizzclick" + "github.com/prebid/prebid-server/v2/adapters/bigoad" + "github.com/prebid/prebid-server/v2/adapters/blasto" "github.com/prebid/prebid-server/v2/adapters/bliink" "github.com/prebid/prebid-server/v2/adapters/blue" "github.com/prebid/prebid-server/v2/adapters/bluesea" @@ -62,12 +67,15 @@ import ( "github.com/prebid/prebid-server/v2/adapters/bwx" cadentaperturemx "github.com/prebid/prebid-server/v2/adapters/cadent_aperture_mx" "github.com/prebid/prebid-server/v2/adapters/ccx" + "github.com/prebid/prebid-server/v2/adapters/cointraffic" "github.com/prebid/prebid-server/v2/adapters/coinzilla" "github.com/prebid/prebid-server/v2/adapters/colossus" "github.com/prebid/prebid-server/v2/adapters/compass" + "github.com/prebid/prebid-server/v2/adapters/concert" "github.com/prebid/prebid-server/v2/adapters/connectad" "github.com/prebid/prebid-server/v2/adapters/consumable" "github.com/prebid/prebid-server/v2/adapters/conversant" + "github.com/prebid/prebid-server/v2/adapters/copper6ssp" "github.com/prebid/prebid-server/v2/adapters/cpmstar" "github.com/prebid/prebid-server/v2/adapters/criteo" "github.com/prebid/prebid-server/v2/adapters/cwire" @@ -76,13 +84,16 @@ import ( "github.com/prebid/prebid-server/v2/adapters/deepintent" "github.com/prebid/prebid-server/v2/adapters/definemedia" "github.com/prebid/prebid-server/v2/adapters/dianomi" + "github.com/prebid/prebid-server/v2/adapters/displayio" "github.com/prebid/prebid-server/v2/adapters/dmx" + "github.com/prebid/prebid-server/v2/adapters/driftpixel" "github.com/prebid/prebid-server/v2/adapters/dxkulture" evolution "github.com/prebid/prebid-server/v2/adapters/e_volution" "github.com/prebid/prebid-server/v2/adapters/edge226" "github.com/prebid/prebid-server/v2/adapters/emtv" "github.com/prebid/prebid-server/v2/adapters/eplanning" "github.com/prebid/prebid-server/v2/adapters/epom" + "github.com/prebid/prebid-server/v2/adapters/escalax" "github.com/prebid/prebid-server/v2/adapters/flipp" "github.com/prebid/prebid-server/v2/adapters/freewheelssp" "github.com/prebid/prebid-server/v2/adapters/frvradn" @@ -110,20 +121,24 @@ import ( "github.com/prebid/prebid-server/v2/adapters/kiviads" "github.com/prebid/prebid-server/v2/adapters/krushmedia" "github.com/prebid/prebid-server/v2/adapters/lemmadigital" - "github.com/prebid/prebid-server/v2/adapters/liftoff" "github.com/prebid/prebid-server/v2/adapters/limelightDigital" lmkiviads "github.com/prebid/prebid-server/v2/adapters/lm_kiviads" "github.com/prebid/prebid-server/v2/adapters/lockerdome" "github.com/prebid/prebid-server/v2/adapters/logan" "github.com/prebid/prebid-server/v2/adapters/logicad" + "github.com/prebid/prebid-server/v2/adapters/loyal" "github.com/prebid/prebid-server/v2/adapters/lunamedia" "github.com/prebid/prebid-server/v2/adapters/mabidder" "github.com/prebid/prebid-server/v2/adapters/madvertise" "github.com/prebid/prebid-server/v2/adapters/marsmedia" + "github.com/prebid/prebid-server/v2/adapters/mediago" "github.com/prebid/prebid-server/v2/adapters/medianet" + "github.com/prebid/prebid-server/v2/adapters/melozen" + "github.com/prebid/prebid-server/v2/adapters/metax" "github.com/prebid/prebid-server/v2/adapters/mgid" "github.com/prebid/prebid-server/v2/adapters/mgidX" "github.com/prebid/prebid-server/v2/adapters/minutemedia" + "github.com/prebid/prebid-server/v2/adapters/missena" "github.com/prebid/prebid-server/v2/adapters/mobfoxpb" "github.com/prebid/prebid-server/v2/adapters/mobilefuse" "github.com/prebid/prebid-server/v2/adapters/motorik" @@ -134,19 +149,25 @@ import ( "github.com/prebid/prebid-server/v2/adapters/openweb" "github.com/prebid/prebid-server/v2/adapters/openx" "github.com/prebid/prebid-server/v2/adapters/operaads" + "github.com/prebid/prebid-server/v2/adapters/oraki" "github.com/prebid/prebid-server/v2/adapters/orbidder" "github.com/prebid/prebid-server/v2/adapters/outbrain" "github.com/prebid/prebid-server/v2/adapters/ownadx" "github.com/prebid/prebid-server/v2/adapters/pangle" "github.com/prebid/prebid-server/v2/adapters/pgamssp" + "github.com/prebid/prebid-server/v2/adapters/playdigo" "github.com/prebid/prebid-server/v2/adapters/pubmatic" "github.com/prebid/prebid-server/v2/adapters/pubnative" + "github.com/prebid/prebid-server/v2/adapters/pubrise" "github.com/prebid/prebid-server/v2/adapters/pulsepoint" "github.com/prebid/prebid-server/v2/adapters/pwbid" + "github.com/prebid/prebid-server/v2/adapters/qt" + "github.com/prebid/prebid-server/v2/adapters/readpeak" "github.com/prebid/prebid-server/v2/adapters/relevantdigital" "github.com/prebid/prebid-server/v2/adapters/revcontent" "github.com/prebid/prebid-server/v2/adapters/richaudience" "github.com/prebid/prebid-server/v2/adapters/rise" + "github.com/prebid/prebid-server/v2/adapters/roulax" "github.com/prebid/prebid-server/v2/adapters/rtbhouse" "github.com/prebid/prebid-server/v2/adapters/rubicon" salunamedia "github.com/prebid/prebid-server/v2/adapters/sa_lunamedia" @@ -162,6 +183,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/smartx" "github.com/prebid/prebid-server/v2/adapters/smartyads" "github.com/prebid/prebid-server/v2/adapters/smilewanted" + "github.com/prebid/prebid-server/v2/adapters/smrtconnect" "github.com/prebid/prebid-server/v2/adapters/sonobi" "github.com/prebid/prebid-server/v2/adapters/sovrn" "github.com/prebid/prebid-server/v2/adapters/sovrnXsp" @@ -171,14 +193,18 @@ import ( "github.com/prebid/prebid-server/v2/adapters/tappx" "github.com/prebid/prebid-server/v2/adapters/teads" "github.com/prebid/prebid-server/v2/adapters/telaria" + "github.com/prebid/prebid-server/v2/adapters/theadx" + "github.com/prebid/prebid-server/v2/adapters/thetradedesk" "github.com/prebid/prebid-server/v2/adapters/tpmn" "github.com/prebid/prebid-server/v2/adapters/trafficgate" "github.com/prebid/prebid-server/v2/adapters/triplelift" "github.com/prebid/prebid-server/v2/adapters/triplelift_native" + "github.com/prebid/prebid-server/v2/adapters/trustedstack" "github.com/prebid/prebid-server/v2/adapters/ucfunnel" "github.com/prebid/prebid-server/v2/adapters/undertone" "github.com/prebid/prebid-server/v2/adapters/unicorn" "github.com/prebid/prebid-server/v2/adapters/unruly" + "github.com/prebid/prebid-server/v2/adapters/vidazoo" "github.com/prebid/prebid-server/v2/adapters/videobyte" "github.com/prebid/prebid-server/v2/adapters/videoheroes" "github.com/prebid/prebid-server/v2/adapters/vidoomy" @@ -186,6 +212,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/visx" "github.com/prebid/prebid-server/v2/adapters/vox" "github.com/prebid/prebid-server/v2/adapters/vrtcal" + "github.com/prebid/prebid-server/v2/adapters/vungle" "github.com/prebid/prebid-server/v2/adapters/xeworks" "github.com/prebid/prebid-server/v2/adapters/yahooAds" "github.com/prebid/prebid-server/v2/adapters/yandex" @@ -215,6 +242,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdkernel: adkernel.Builder, openrtb_ext.BidderAdkernelAdn: adkernelAdn.Builder, openrtb_ext.BidderAdman: adman.Builder, + openrtb_ext.BidderAdmatic: admatic.Builder, openrtb_ext.BidderAdmixer: admixer.Builder, openrtb_ext.BidderAdnuntius: adnuntius.Builder, openrtb_ext.BidderAdOcean: adocean.Builder, @@ -228,6 +256,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdtarget: adtarget.Builder, openrtb_ext.BidderAdtrgtme: adtrgtme.Builder, openrtb_ext.BidderAdtelligent: adtelligent.Builder, + openrtb_ext.BidderAdTonos: adtonos.Builder, openrtb_ext.BidderAdvangelists: advangelists.Builder, openrtb_ext.BidderAdView: adview.Builder, openrtb_ext.BidderAdxcg: adxcg.Builder, @@ -240,6 +269,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderApacdex: apacdex.Builder, openrtb_ext.BidderAppnexus: appnexus.Builder, openrtb_ext.BidderAppush: appush.Builder, + openrtb_ext.BidderAso: aso.Builder, openrtb_ext.BidderAudienceNetwork: audienceNetwork.Builder, openrtb_ext.BidderAutomatad: automatad.Builder, openrtb_ext.BidderAvocet: avocet.Builder, @@ -251,10 +281,12 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBetween: between.Builder, openrtb_ext.BidderBeyondMedia: beyondmedia.Builder, openrtb_ext.BidderBidmachine: bidmachine.Builder, + openrtb_ext.BidderBidmatic: bidmatic.Builder, openrtb_ext.BidderBidmyadz: bidmyadz.Builder, openrtb_ext.BidderBidsCube: bidscube.Builder, openrtb_ext.BidderBidstack: bidstack.Builder, - openrtb_ext.BidderBizzclick: bizzclick.Builder, + openrtb_ext.BidderBigoAd: bigoad.Builder, + openrtb_ext.BidderBlasto: blasto.Builder, openrtb_ext.BidderBliink: bliink.Builder, openrtb_ext.BidderBlue: blue.Builder, openrtb_ext.BidderBluesea: bluesea.Builder, @@ -264,12 +296,15 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBWX: bwx.Builder, openrtb_ext.BidderCadentApertureMX: cadentaperturemx.Builder, openrtb_ext.BidderCcx: ccx.Builder, + openrtb_ext.BidderCointraffic: cointraffic.Builder, openrtb_ext.BidderCoinzilla: coinzilla.Builder, openrtb_ext.BidderColossus: colossus.Builder, openrtb_ext.BidderCompass: compass.Builder, + openrtb_ext.BidderConcert: concert.Builder, openrtb_ext.BidderConnectAd: connectad.Builder, openrtb_ext.BidderConsumable: consumable.Builder, openrtb_ext.BidderConversant: conversant.Builder, + openrtb_ext.BidderCopper6ssp: copper6ssp.Builder, openrtb_ext.BidderCpmstar: cpmstar.Builder, openrtb_ext.BidderCriteo: criteo.Builder, openrtb_ext.BidderCWire: cwire.Builder, @@ -278,13 +313,16 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderDeepintent: deepintent.Builder, openrtb_ext.BidderDefinemedia: definemedia.Builder, openrtb_ext.BidderDianomi: dianomi.Builder, + openrtb_ext.BidderDisplayio: displayio.Builder, openrtb_ext.BidderEdge226: edge226.Builder, openrtb_ext.BidderDmx: dmx.Builder, openrtb_ext.BidderDXKulture: dxkulture.Builder, + openrtb_ext.BidderDriftPixel: driftpixel.Builder, openrtb_ext.BidderEmtv: emtv.Builder, openrtb_ext.BidderEmxDigital: cadentaperturemx.Builder, openrtb_ext.BidderEPlanning: eplanning.Builder, openrtb_ext.BidderEpom: epom.Builder, + openrtb_ext.BidderEscalax: escalax.Builder, openrtb_ext.BidderEVolution: evolution.Builder, openrtb_ext.BidderFlipp: flipp.Builder, openrtb_ext.BidderFreewheelSSP: freewheelssp.Builder, @@ -314,20 +352,25 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderLmKiviads: lmkiviads.Builder, openrtb_ext.BidderKrushmedia: krushmedia.Builder, openrtb_ext.BidderLemmadigital: lemmadigital.Builder, - openrtb_ext.BidderLiftoff: liftoff.Builder, + openrtb_ext.BidderVungle: vungle.Builder, openrtb_ext.BidderLimelightDigital: limelightDigital.Builder, openrtb_ext.BidderLockerDome: lockerdome.Builder, openrtb_ext.BidderLogan: logan.Builder, openrtb_ext.BidderLogicad: logicad.Builder, + openrtb_ext.BidderLoyal: loyal.Builder, openrtb_ext.BidderLunaMedia: lunamedia.Builder, openrtb_ext.BidderMabidder: mabidder.Builder, openrtb_ext.BidderMadvertise: madvertise.Builder, openrtb_ext.BidderMarsmedia: marsmedia.Builder, openrtb_ext.BidderMediafuse: appnexus.Builder, + openrtb_ext.BidderMediaGo: mediago.Builder, openrtb_ext.BidderMedianet: medianet.Builder, + openrtb_ext.BidderMeloZen: melozen.Builder, + openrtb_ext.BidderMetaX: metax.Builder, openrtb_ext.BidderMgid: mgid.Builder, openrtb_ext.BidderMgidX: mgidX.Builder, openrtb_ext.BidderMinuteMedia: minutemedia.Builder, + openrtb_ext.BidderMissena: missena.Builder, openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, openrtb_ext.BidderMotorik: motorik.Builder, @@ -338,19 +381,25 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderOpenWeb: openweb.Builder, openrtb_ext.BidderOpenx: openx.Builder, openrtb_ext.BidderOperaads: operaads.Builder, + openrtb_ext.BidderOraki: oraki.Builder, openrtb_ext.BidderOrbidder: orbidder.Builder, openrtb_ext.BidderOutbrain: outbrain.Builder, openrtb_ext.BidderOwnAdx: ownadx.Builder, openrtb_ext.BidderPangle: pangle.Builder, openrtb_ext.BidderPGAMSsp: pgamssp.Builder, + openrtb_ext.BidderPlaydigo: playdigo.Builder, openrtb_ext.BidderPubmatic: pubmatic.Builder, openrtb_ext.BidderPubnative: pubnative.Builder, + openrtb_ext.BidderPubrise: pubrise.Builder, openrtb_ext.BidderPulsepoint: pulsepoint.Builder, openrtb_ext.BidderPWBid: pwbid.Builder, + openrtb_ext.BidderQT: qt.Builder, + openrtb_ext.BidderReadpeak: readpeak.Builder, openrtb_ext.BidderRelevantDigital: relevantdigital.Builder, openrtb_ext.BidderRevcontent: revcontent.Builder, openrtb_ext.BidderRichaudience: richaudience.Builder, openrtb_ext.BidderRise: rise.Builder, + openrtb_ext.BidderRoulax: roulax.Builder, openrtb_ext.BidderRTBHouse: rtbhouse.Builder, openrtb_ext.BidderRubicon: rubicon.Builder, openrtb_ext.BidderSeedingAlliance: seedingAlliance.Builder, @@ -366,6 +415,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderSmartx: smartx.Builder, openrtb_ext.BidderSmartyAds: smartyads.Builder, openrtb_ext.BidderSmileWanted: smilewanted.Builder, + openrtb_ext.BidderSmrtconnect: smrtconnect.Builder, openrtb_ext.BidderSonobi: sonobi.Builder, openrtb_ext.BidderSovrn: sovrn.Builder, openrtb_ext.BidderSovrnXsp: sovrnXsp.Builder, @@ -375,14 +425,18 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderTappx: tappx.Builder, openrtb_ext.BidderTeads: teads.Builder, openrtb_ext.BidderTelaria: telaria.Builder, + openrtb_ext.BidderTheadx: theadx.Builder, + openrtb_ext.BidderTheTradeDesk: thetradedesk.Builder, openrtb_ext.BidderTpmn: tpmn.Builder, openrtb_ext.BidderTrafficGate: trafficgate.Builder, openrtb_ext.BidderTriplelift: triplelift.Builder, openrtb_ext.BidderTripleliftNative: triplelift_native.Builder, + openrtb_ext.BidderTrustedstack: trustedstack.Builder, openrtb_ext.BidderUcfunnel: ucfunnel.Builder, openrtb_ext.BidderUndertone: undertone.Builder, openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, + openrtb_ext.BidderVidazoo: vidazoo.Builder, openrtb_ext.BidderVideoByte: videobyte.Builder, openrtb_ext.BidderVideoHeroes: videoheroes.Builder, openrtb_ext.BidderVidoomy: vidoomy.Builder, diff --git a/exchange/bidder.go b/exchange/bidder.go index 8b54f9847bb..59f4623b252 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -76,12 +76,14 @@ type bidRequestOptions struct { type extraBidderRespInfo struct { respProcessingStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } type extraAuctionResponseInfo struct { fledge *openrtb_ext.Fledge bidsFound bool bidderResponseStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } const ImpIdReqBody = "Stored bid response for impression id: " @@ -96,7 +98,7 @@ const ( // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter" // (which is being phased out and replaced by Bidder for OpenRTB auctions) func AdaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me metrics.MetricsEngine, name openrtb_ext.BidderName, debugInfo *config.DebugInfo, endpointCompression string) AdaptedBidder { - return &bidderAdapter{ + return &BidderAdapter{ Bidder: bidder, BidderName: name, Client: client, @@ -117,7 +119,7 @@ func parseDebugInfo(info *config.DebugInfo) bool { return info.Allow } -type bidderAdapter struct { +type BidderAdapter struct { Bidder adapters.Bidder BidderName openrtb_ext.BidderName Client *http.Client @@ -132,9 +134,10 @@ type bidderAdapterConfig struct { EndpointCompression string } -func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { +func (bidder *BidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { request := openrtb_ext.RequestWrapper{BidRequest: bidderRequest.BidRequest} reject := hookExecutor.ExecuteBidderRequestStage(&request, string(bidderRequest.BidderName)) + seatNonBidBuilder := SeatNonBidBuilder{} if reject != nil { return nil, extraBidderRespInfo{}, []error{reject} } @@ -397,13 +400,17 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } } else { errs = append(errs, httpInfo.err) + nonBidReason := httpInfoToNonBidReason(httpInfo) + seatNonBidBuilder.rejectImps(httpInfo.request.ImpIDs, nonBidReason, string(bidderRequest.BidderName)) } } + seatBids := make([]*entities.PbsOrtbSeatBid, 0, len(seatBidMap)) for _, seatBid := range seatBidMap { seatBids = append(seatBids, seatBid) } + extraRespInfo.seatNonBidBuilder = seatNonBidBuilder return seatBids, extraRespInfo, errs } @@ -518,11 +525,11 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { // doRequest makes a request, handles the response, and returns the data needed by the // Bidder interface. -func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { +func (bidder *BidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { return bidder.doRequestImpl(ctx, req, glog.Warningf, bidderRequestStartTime, tmaxAdjustments) } -func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { +func (bidder *BidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { requestBody, err := getRequestBody(req, bidder.config.EndpointCompression) if err != nil { return &httpCallInfo{ @@ -609,7 +616,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re } } -func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData, logger util.LogMsg) { +func (bidder *BidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData, logger util.LogMsg) { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() toReq, errL := timeoutBidder.MakeTimeoutNotification(req) @@ -659,7 +666,7 @@ type httpCallInfo struct { // This function adds an httptrace.ClientTrace object to the context so, if connection with the bidder // endpoint is established, we can keep track of whether the connection was newly created, reused, and // the time from the connection request, to the connection creation. -func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context { +func (bidder *BidderAdapter) addClientTrace(ctx context.Context) context.Context { var connStart, dnsStart, tlsStart time.Time trace := &httptrace.ClientTrace{ diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 9f092b7dea3..dfe58b0bed1 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -9,11 +9,15 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/http/httptrace" + "net/url" + "os" "sort" "strings" + "syscall" "testing" "time" @@ -552,7 +556,7 @@ func TestBidderTimeout(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: &mixedMultiBidder{}, BidderName: openrtb_ext.BidderAppnexus, Client: server.Client(), @@ -574,7 +578,7 @@ func TestBidderTimeout(t *testing.T) { // TestInvalidRequest makes sure that bidderAdapter.doRequest returns errors on bad requests. func TestInvalidRequest(t *testing.T) { server := httptest.NewServer(mockHandler(200, "getBody", "postBody")) - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: &mixedMultiBidder{}, Client: server.Client(), } @@ -595,7 +599,7 @@ func TestConnectionClose(t *testing.T) { }) server = httptest.NewServer(handler) - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: &mixedMultiBidder{}, Client: server.Client(), BidderName: openrtb_ext.BidderAppnexus, @@ -2145,7 +2149,7 @@ func TestCallRecordDNSTime(t *testing.T) { // Instantiate the bidder that will send the request. We'll make sure to use an // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) // gets called - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: &mixedMultiBidder{}, Client: &http.Client{Transport: DNSDoneTripper{}}, me: metricsMock, @@ -2169,7 +2173,7 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) { // Instantiate the bidder that will send the request. We'll make sure to use an // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) // gets called - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: &mixedMultiBidder{}, Client: &http.Client{Transport: TLSHandshakeTripper{}}, me: metricsMock, @@ -2197,7 +2201,7 @@ func TestTimeoutNotificationOff(t *testing.T) { Headers: http.Header{}, }, } - bidder := &bidderAdapter{ + bidder := &BidderAdapter{ Bidder: bidderImpl, Client: server.Client(), config: bidderAdapterConfig{Debug: config.Debug{}}, @@ -2231,7 +2235,7 @@ func TestTimeoutNotificationOn(t *testing.T) { // Wrap with BidderInfo to mimic exchange.go flow. bidderWrappedWithInfo := wrapWithBidderInfo(bidder) - bidderAdapter := &bidderAdapter{ + bidderAdapter := &BidderAdapter{ Bidder: bidderWrappedWithInfo, Client: server.Client(), config: bidderAdapterConfig{ @@ -3099,6 +3103,148 @@ func TestGetBidType(t *testing.T) { } } +func TestSeatNonBid(t *testing.T) { + type args struct { + BidRequest *openrtb2.BidRequest + Seat string + SeatRequests []*adapters.RequestData + BidderResponse func() (*http.Response, error) + client *http.Client + } + type expect struct { + seatBids []*entities.PbsOrtbSeatBid + seatNonBids SeatNonBidBuilder + errors []error + } + testCases := []struct { + name string + args args + expect expect + }{ + { + name: "NBR_101_timeout_for_context_deadline_exceeded", + args: args{ + Seat: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{ID: "1234"}}, + }, + SeatRequests: []*adapters.RequestData{{ImpIDs: []string{"1234"}}}, + BidderResponse: func() (*http.Response, error) { return nil, context.DeadlineExceeded }, + client: &http.Client{Timeout: time.Nanosecond}, // for timeout + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{ + "pubmatic": {{ + ImpId: "1234", + StatusCode: int(ErrorTimeout), + }}, + }, + errors: []error{&errortypes.Timeout{Message: context.DeadlineExceeded.Error()}}, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", Seat: "pubmatic", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + }, + }, { + name: "NBR_103_Bidder_Unreachable_Connection_Refused", + args: args{ + Seat: "appnexus", + SeatRequests: []*adapters.RequestData{{ImpIDs: []string{"1234", "4567"}}}, + BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1234"}, {ID: "4567"}}}, + BidderResponse: func() (*http.Response, error) { + return nil, &net.OpError{Err: os.NewSyscallError(syscall.ECONNREFUSED.Error(), syscall.ECONNREFUSED)} + }, + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{ + "appnexus": { + {ImpId: "1234", StatusCode: int(ErrorBidderUnreachable)}, + {ImpId: "4567", StatusCode: int(ErrorBidderUnreachable)}, + }, + }, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", Seat: "appnexus", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + errors: []error{&url.Error{Op: "Get", URL: "", Err: &net.OpError{Err: os.NewSyscallError(syscall.ECONNREFUSED.Error(), syscall.ECONNREFUSED)}}}, + }, + }, { + name: "no_impids_populated_in_request_data", + args: args{ + SeatRequests: []*adapters.RequestData{{ + ImpIDs: nil, // no imp ids + }}, + BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1234"}}}, + BidderResponse: func() (*http.Response, error) { + return nil, errors.New("some_error") + }, + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{}, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + errors: []error{&url.Error{Op: "Get", URL: "", Err: errors.New("some_error")}}, + }, + }, + } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + mockBidder := &mockBidder{} + mockBidder.On("MakeRequests", mock.Anything, mock.Anything).Return(test.args.SeatRequests, []error(nil)) + mockMetricsEngine := &metrics.MetricsEngineMock{} + mockMetricsEngine.On("RecordOverheadTime", mock.Anything, mock.Anything).Return(nil) + mockMetricsEngine.On("RecordBidderServerResponseTime", mock.Anything).Return(nil) + roundTrip := &mockRoundTripper{} + roundTrip.On("RoundTrip", mock.Anything).Return(test.args.BidderResponse()) + client := &http.Client{ + Transport: roundTrip, + Timeout: 0, + } + if test.args.client != nil { + client.Timeout = test.args.client.Timeout + } + bidder := AdaptBidder(mockBidder, client, &config.Configuration{}, mockMetricsEngine, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, test.args.Seat) + + ctx := context.Background() + if client.Timeout > 0 { + ctxTimeout, cancel := context.WithTimeout(ctx, client.Timeout) + ctx = ctxTimeout + defer cancel() + } + seatBids, responseExtra, errors := bidder.requestBid(ctx, BidderRequest{ + BidRequest: test.args.BidRequest, + BidderName: openrtb_ext.BidderName(test.args.Seat), + }, nil, &adapters.ExtraRequestInfo{}, &MockSigner{}, bidRequestOptions{}, openrtb_ext.ExtAlternateBidderCodes{}, hookexecution.EmptyHookExecutor{}, nil) + assert.Equal(t, test.expect.seatBids, seatBids) + assert.Equal(t, test.expect.seatNonBids, responseExtra.seatNonBidBuilder) + assert.Equal(t, test.expect.errors, errors) + for _, nonBids := range responseExtra.seatNonBidBuilder { + for _, nonBid := range nonBids { + for _, seatBid := range seatBids { + for _, bid := range seatBid.Bids { + // ensure non bids are not present in seat bids + if nonBid.ImpId == bid.Bid.ImpID { + assert.Fail(t, "imp id [%s] present in both seat bid and non seat bid", nonBid.ImpId) + } + } + } + } + } + }) + } +} + +type mockRoundTripper struct { + mock.Mock +} + +func (rt *mockRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + args := rt.Called(request) + var response *http.Response + if args.Get(0) != nil { + response = args.Get(0).(*http.Response) + } + var err error + if args.Get(1) != nil { + err = args.Get(1).(error) + } + + return response, err +} + type mockBidderTmaxCtx struct { startTime, deadline, now time.Time ok bool @@ -3238,7 +3384,7 @@ func TestDoRequestImplWithTmax(t *testing.T) { Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), } - bidderAdapter := bidderAdapter{ + bidderAdapter := BidderAdapter{ me: &metricsConfig.NilMetricsEngine{}, Client: server.Client(), } @@ -3313,7 +3459,7 @@ func TestDoRequestImplWithTmaxTimeout(t *testing.T) { metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() metricsMock.On("RecordTMaxTimeout").Once() - bidderAdapter := bidderAdapter{ + bidderAdapter := BidderAdapter{ me: metricsMock, Client: server.Client(), } diff --git a/exchange/exchange.go b/exchange/exchange.go index b9dae6725c6..a8787ac3db2 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" "github.com/prebid/prebid-server/v2/adapters" @@ -94,6 +95,8 @@ type seatResponseExtra struct { // httpCalls is the list of debugging info. It should only be populated if the request.test == 1. // This will become response.ext.debug.httpcalls.{bidder} on the final Response. HttpCalls []*openrtb_ext.ExtHttpCall + // NonBid contains non bid reason information + NonBid *openrtb_ext.NonBid } type bidResponseWrapper struct { @@ -102,10 +105,11 @@ type bidResponseWrapper struct { bidder openrtb_ext.BidderName adapter openrtb_ext.BidderName bidderResponseStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } type BidIDGenerator interface { - New() (string, error) + New(bidder string) (string, error) Enabled() bool } @@ -117,7 +121,7 @@ func (big *bidIDGenerator) Enabled() bool { return big.enabled } -func (big *bidIDGenerator) New() (string, error) { +func (big *bidIDGenerator) New(bidder string) (string, error) { rawUuid, err := uuid.NewV4() return rawUuid.String(), err } @@ -132,7 +136,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool { return rand.Intn(100) < 50 } -func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange { +func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, requestValidator ortb.RequestValidator, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange { bidderToSyncerKey := map[string]string{} for bidder, syncer := range syncersByBidder { bidderToSyncerKey[bidder] = syncer.Key() @@ -155,6 +159,7 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid gdprPermsBuilder: gdprPermsBuilder, hostSChainNode: cfg.HostSChainNode, bidderInfo: infos, + requestValidator: requestValidator, } return &exchange{ @@ -313,6 +318,19 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog // Make our best guess if GDPR applies gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequestWrapper) + gdprSignal, err := getGDPR(r.BidRequestWrapper) + if err != nil { + return nil, err + } + channelEnabled := r.TCF2Config.ChannelEnabled(channelTypeMap[r.LegacyLabels.RType]) + gdprEnforced := enforceGDPR(gdprSignal, gdprDefaultValue, channelEnabled) + dsaWriter := dsa.Writer{ + Config: r.Account.Privacy.DSA, + GDPRInScope: gdprEnforced, + } + if err := dsaWriter.Write(r.BidRequestWrapper); err != nil { + return nil, err + } // rebuild/resync the request in the request wrapper. if err := r.BidRequestWrapper.RebuildRequest(); err != nil { @@ -324,7 +342,12 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog Prebid: *requestExtPrebid, SChain: requestExt.GetSChain(), } - bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue, bidAdjustmentFactors) + bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprSignal, gdprEnforced, bidAdjustmentFactors) + for _, err := range errs { + if errortypes.ReadCode(err) == errortypes.InvalidImpFirstPartyDataErrorCode { + return nil, err + } + } errs = append(errs, floorErrs...) mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments) @@ -353,7 +376,8 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog fledge *openrtb_ext.Fledge anyBidsReturned bool // List of bidders we have requests for. - liveAdapters []openrtb_ext.BidderName + liveAdapters []openrtb_ext.BidderName + seatNonBidBuilder SeatNonBidBuilder = SeatNonBidBuilder{} ) if len(r.StoredAuctionResponses) > 0 { @@ -379,13 +403,15 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog fledge = extraRespInfo.fledge anyBidsReturned = extraRespInfo.bidsFound r.BidderResponseStartTime = extraRespInfo.bidderResponseStartTime + if extraRespInfo.seatNonBidBuilder != nil { + seatNonBidBuilder = extraRespInfo.seatNonBidBuilder + } } var ( auc *auction cacheErrs []error bidResponseExt *openrtb_ext.ExtBidResponse - seatNonBids = nonBids{} ) if anyBidsReturned { @@ -399,6 +425,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog errs = append(errs, &errortypes.Warning{ Message: fmt.Sprintf("%s bid id %s rejected - bid price %.4f %s is less than bid floor %.4f %s for imp %s", rejectedBid.Seat, rejectedBid.Bids[0].Bid.ID, rejectedBid.Bids[0].Bid.Price, rejectedBid.Currency, rejectedBid.Bids[0].BidFloors.FloorValue, rejectedBid.Bids[0].BidFloors.FloorCurrency, rejectedBid.Bids[0].Bid.ImpID), WarningCode: errortypes.FloorBidRejectionWarningCode}) + rejectionReason := ResponseRejectedBelowFloor + if rejectedBid.Bids[0].Bid.DealID != "" { + rejectionReason = ResponseRejectedBelowDealFloor + } + seatNonBidBuilder.rejectBid(rejectedBid.Bids[0], int(rejectionReason), rejectedBid.Seat) } } @@ -406,7 +437,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog //If includebrandcategory is present in ext then CE feature is on. if requestExtPrebid.Targeting != nil && requestExtPrebid.Targeting.IncludeBrandCategory != nil { var rejections []string - bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBids) + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBidBuilder) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } @@ -416,10 +447,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog } if e.bidIDGenerator.Enabled() { - for _, seatBid := range adapterBids { - for _, pbsBid := range seatBid.Bids { - pbsBid.GeneratedBidID, err = e.bidIDGenerator.New() - if err != nil { + for bidder, seatBid := range adapterBids { + for i := range seatBid.Bids { + if bidID, err := e.bidIDGenerator.New(bidder.String()); err == nil { + seatBid.Bids[i].GeneratedBidID = bidID + } else { errs = append(errs, errors.New("Error generating bid.ext.prebid.bidid")) } } @@ -459,7 +491,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog errs = append(errs, cacheErrs...) } - targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) + if targData.includeWinners || targData.includeBidderKeys || targData.includeFormat { + targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) + } } bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) } else { @@ -485,6 +519,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog } for _, warning := range r.Warnings { + if errortypes.ReadScope(warning) == errortypes.ScopeDebug && !responseDebugAllow { + continue + } generalWarning := openrtb_ext.ExtBidderMessage{ Code: errortypes.ReadCode(warning), Message: warning.Error(), @@ -495,14 +532,14 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations) // Build the response - bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids) + bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBidBuilder) bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute) bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) if err != nil { return nil, err } - bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBids) + bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBidBuilder) return &AuctionResponse{ BidResponse: bidResponse, @@ -682,7 +719,7 @@ func (e *exchange) getAllBids( adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests)) adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) chBids := make(chan *bidResponseWrapper, len(bidderRequests)) - extraRespInfo := extraAuctionResponseInfo{} + extraRespInfo := extraAuctionResponseInfo{seatNonBidBuilder: SeatNonBidBuilder{}} e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime)) @@ -722,6 +759,7 @@ func (e *exchange) getAllBids( // Add in time reporting elapsed := time.Since(start) brw.adapterSeatBids = seatBids + brw.seatNonBidBuilder = extraBidderRespInfo.seatNonBidBuilder // Structure to record extra tracking data generated during bidding ae := new(seatResponseExtra) ae.ResponseTimeMillis = int(elapsed / time.Millisecond) @@ -774,6 +812,10 @@ func (e *exchange) getAllBids( } //but we need to add all bidders data to adapterExtra to have metrics and other metadata adapterExtra[brw.bidder] = brw.adapterExtra + + // collect adapter non bids + extraRespInfo.seatNonBidBuilder.append(brw.seatNonBidBuilder) + } return adapterBids, adapterExtra, extraRespInfo @@ -891,7 +933,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.BidResponse { bidResponse := new(openrtb2.BidResponse) bidResponse.ID = bidRequest.ID @@ -906,7 +948,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ for a, adapterSeatBids := range adapterSeatBids { //while processing every single bib, do we need to handle categories here? if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 { - sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBids) + sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBidBuilder) seatBids = append(seatBids, *sb) bidResponse.Cur = adapterSeatBids.Currency } @@ -926,7 +968,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBids *nonBids) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { +func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBidBuilder *SeatNonBidBuilder) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -988,7 +1030,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid bidsToRemove = append(bidsToRemove, bidInd) rejections = updateRejections(rejections, bidID, "Bid did not contain a category") - seatNonBids.addBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) continue } if translateCategories { @@ -1225,14 +1267,14 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en // Return an openrtb seatBid for a bidder // buildBidResponse is responsible for ensuring nil bid seatbids are not included -func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid { +func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.SeatBid { seatBid := &openrtb2.SeatBid{ Seat: adapter.String(), Group: 0, // Prebid cannot support roadblocking } var errList []error - seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBids) + seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBidBuilder) if len(errList) > 0 { adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) } @@ -1240,24 +1282,24 @@ func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter open return seatBid } -func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) { +func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBidBuilder *SeatNonBidBuilder) ([]openrtb2.Bid, []error) { result := make([]openrtb2.Bid, 0, len(bids)) errs := make([]error, 0, 1) for _, bid := range bids { - if !dsa.Validate(bidRequest, bid) { - RequiredError := openrtb_ext.ExtBidderMessage{ + if err := dsa.Validate(bidRequest, bid); err != nil { + dsaMessage := openrtb_ext.ExtBidderMessage{ Code: errortypes.InvalidBidResponseDSAWarningCode, - Message: "bid response rejected: DSA object missing when required", + Message: fmt.Sprintf("bid rejected: %s", err.Error()), } - bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError) + bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) - seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedGeneral), adapter.String()) continue // Don't add bid to result } if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) { - seatNonBids.addBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.BidType == openrtb_ext.BidTypeBanner { @@ -1266,7 +1308,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea if _, ok := impExtInfoMap[bid.Bid.ImpID]; ok { if e.bidValidationEnforcement.SecureMarkup == config.ValidationEnforce && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { if !e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup) { - seatNonBids.addBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { @@ -1293,7 +1335,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea } } - if bidExtJSON, err := makeBidExtJSON(bid.Bid.Ext, bidExtPrebid, impExtInfoMap, bid.Bid.ImpID, bid.OriginalBidCPM, bid.OriginalBidCur, adapter); err != nil { + if bidExtJSON, err := makeBidExtJSON(bid.Bid.Ext, bidExtPrebid, impExtInfoMap, bid.Bid.ImpID, bid.OriginalBidCPM, bid.OriginalBidCur, bid.AdapterCode); err != nil { errs = append(errs, err) } else { result = append(result, *bid.Bid) @@ -1566,8 +1608,8 @@ func setErrorMessageSecureMarkup(validationType string) string { } // setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid -func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBids) *openrtb_ext.ExtBidResponse { - if len(seatNonBids.seatNonBidsMap) == 0 { +func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBidBuilder SeatNonBidBuilder) *openrtb_ext.ExtBidResponse { + if len(seatNonBidBuilder) == 0 { return bidResponseExt } if bidResponseExt == nil { @@ -1577,6 +1619,6 @@ func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBi bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} } - bidResponseExt.Prebid.SeatNonBid = seatNonBids.get() + bidResponseExt.Prebid.SeatNonBid = seatNonBidBuilder.Slice() return bidResponseExt } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index e484e21a42e..7014448f14d 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -35,10 +35,12 @@ import ( metricsConf "github.com/prebid/prebid-server/v2/metrics/config" metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" pbc "github.com/prebid/prebid-server/v2/prebid_cache_client" "github.com/prebid/prebid-server/v2/privacy" "github.com/prebid/prebid-server/v2/stored_requests" "github.com/prebid/prebid-server/v2/stored_requests/backends/file_fetcher" + "github.com/prebid/prebid-server/v2/stored_responses" "github.com/prebid/prebid-server/v2/usersync" "github.com/prebid/prebid-server/v2/util/jsonutil" "github.com/prebid/prebid-server/v2/util/ptrutil" @@ -82,7 +84,7 @@ func TestNewExchange(t *testing.T) { }, }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) for _, bidderName := range knownAdapters { if _, ok := e.adapterMap[bidderName]; !ok { if biddersInfo[string(bidderName)].IsEnabled() { @@ -132,7 +134,7 @@ func TestCharacterEscape(t *testing.T) { }, }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs //liveAdapters []openrtb_ext.BidderName, @@ -170,7 +172,7 @@ func TestCharacterEscape(t *testing.T) { var errList []error // 4) Build bid response - bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &nonBids{}) + bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &SeatNonBidBuilder{}) // 5) Assert we have no errors and one '&' character as we are supposed to if len(errList) > 0 { @@ -365,7 +367,7 @@ func TestDebugBehaviour(t *testing.T) { if test.generateWarnings { var errL []error errL = append(errL, &errortypes.Warning{ - Message: fmt.Sprintf("CCPA consent test warning."), + Message: "CCPA consent test warning.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) auctionRequest.Warnings = errL } @@ -663,7 +665,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) { }.Builder e.currencyConverter = mockCurrencyConverter e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -768,7 +770,7 @@ func TestAdapterCurrency(t *testing.T) { }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, - bidIDGenerator: &mockBidIDGenerator{false, false}, + bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ openrtb_ext.BidderName("appnexus"): AdaptBidder(mockBidder, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderName("appnexus"), nil, ""), }, @@ -846,7 +848,7 @@ func TestFloorsSignalling(t *testing.T) { }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, - bidIDGenerator: &mockBidIDGenerator{false, false}, + bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, priceFloorEnabled: true, priceFloorFetcher: &mockPriceFloorFetcher{}, } @@ -978,6 +980,7 @@ func TestFloorsSignalling(t *testing.T) { Account: config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) @@ -1129,7 +1132,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) { }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -1234,7 +1237,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { }, }.Builder - e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, pbc, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs liveAdapters := []openrtb_ext.BidderName{bidderName} @@ -1340,7 +1343,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { var errList []error // 4) Build bid response - bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &nonBids{}) + bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &SeatNonBidBuilder{}) expectedBidResponse := &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ @@ -1430,7 +1433,7 @@ func TestBidReturnsCreative(t *testing.T) { //Run tests for _, test := range testCases { - resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &nonBids{}) + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &SeatNonBidBuilder{}) assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) @@ -1593,7 +1596,7 @@ func TestBidResponseCurrency(t *testing.T) { }, }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -1637,7 +1640,7 @@ func TestBidResponseCurrency(t *testing.T) { Price: 9.517803, W: 300, H: 250, - Ext: json.RawMessage(`{"origbidcpm":9.517803,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"banner"}}`), + Ext: json.RawMessage(`{"origbidcpm":9.517803,"prebid":{"meta":{},"type":"banner"}}`), }, }, }, @@ -1715,7 +1718,7 @@ func TestBidResponseCurrency(t *testing.T) { } // Run tests for i := range testCases { - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &nonBids{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &SeatNonBidBuilder{}) assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) } } @@ -1741,7 +1744,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { t.Fatalf("Error intializing adapters: %v", adaptersErr) } - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -1767,7 +1770,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { H: 250, Ext: nil, } - aPbsOrtbBidArr := []*entities.PbsOrtbBid{{Bid: sampleBid, BidType: openrtb_ext.BidTypeVideo}} + aPbsOrtbBidArr := []*entities.PbsOrtbBid{{Bid: sampleBid, BidType: openrtb_ext.BidTypeVideo, AdapterCode: openrtb_ext.BidderAppnexus}} adapterBids := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ openrtb_ext.BidderName("appnexus"): { @@ -1783,7 +1786,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}` - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &nonBids{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &SeatNonBidBuilder{}) resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext) assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect") @@ -1835,7 +1838,7 @@ func TestRaceIntegration(t *testing.T) { }, }.Builder - ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + ex := NewExchange(adapters, &wellBehavedCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) _, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) @@ -1933,7 +1936,7 @@ func TestPanicRecovery(t *testing.T) { }, }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) chBids := make(chan *bidResponseWrapper, 1) panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) { @@ -2003,7 +2006,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) { allowAllBidders: true, }, }.Builder - e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, &mockCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{} e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{} @@ -2105,8 +2108,8 @@ func loadFile(filename string) (*exchangeSpec, error) { } func runSpec(t *testing.T, filename string, spec *exchangeSpec) { - aliases, errs := parseAliases(&spec.IncomingRequest.OrtbRequest) - if len(errs) != 0 { + aliases, err := parseRequestAliases(spec.IncomingRequest.OrtbRequest) + if err != nil { t.Fatalf("%s: Failed to parse aliases", filename) } @@ -2139,11 +2142,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { }, }, } - bidIdGenerator := &mockBidIDGenerator{} + bidIdGenerator := &fakeBidIDGenerator{} if spec.BidIDGenerator != nil { - *bidIdGenerator = *spec.BidIDGenerator + bidIdGenerator = spec.BidIDGenerator } - ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.FloorsEnabled, spec.HostConfigBidValidation, spec.Server) + ex := newExchangeForTests(t, filename, aliases, privacyConfig, bidIdGenerator, spec) biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest) debugLog := &DebugLog{} if spec.DebugLog != nil { @@ -2170,7 +2173,13 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { impExtInfoMap[impID] = ImpExtInfo{} } - activityControl := privacy.NewActivityControl(spec.AccountPrivacy) + if spec.AccountPrivacy.DSA != nil && len(spec.AccountPrivacy.DSA.Default) > 0 { + if err := jsonutil.Unmarshal([]byte(spec.AccountPrivacy.DSA.Default), &spec.AccountPrivacy.DSA.DefaultUnpacked); err != nil { + t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error()) + } + } + + activityControl := privacy.NewActivityControl(&spec.AccountPrivacy) auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}, @@ -2180,7 +2189,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { Enabled: spec.EventsEnabled, }, DebugAllow: true, - PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled}, + PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled, EnforceDealFloors: spec.AccountEnforceDealFloors}, + Privacy: spec.AccountPrivacy, Validations: spec.AccountConfigBidValidation, }, UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), @@ -2327,7 +2337,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string { - if splitImps, err := splitImps(req.Imp); err != nil { + + if splitImps, err := splitImps(req.Imp, &mockRequestValidator{}, nil, false, nil); err != nil { t.Errorf("%s: Failed to parse Bidders from request: %v", context, err) return nil } else { @@ -2363,11 +2374,11 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidRespons } } -func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag, floorsFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange { - bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(expectations)) - bidderInfos := make(config.BidderInfos, len(expectations)) +func newExchangeForTests(t *testing.T, filename string, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, exSpec *exchangeSpec) Exchange { + bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(exSpec.OutgoingRequests)) + bidderInfos := make(config.BidderInfos, len(exSpec.OutgoingRequests)) for _, bidderName := range openrtb_ext.CoreBidderNames() { - if spec, ok := expectations[string(bidderName)]; ok { + if spec, ok := exSpec.OutgoingRequests[string(bidderName)]; ok { bidderAdapters[bidderName] = &validatingBidder{ t: t, fileName: filename, @@ -2375,12 +2386,18 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] expectations: map[string]*bidderRequest{string(bidderName): spec.ExpectedRequest}, mockResponses: map[string]bidderResponse{string(bidderName): spec.MockResponse}, } - bidderInfos[string(bidderName)] = config.BidderInfo{ModifyingVastXmlAllowed: spec.ModifyingVastXmlAllowed} + ortbVersion, _ := exSpec.ORTBVersion[string(bidderName)] + bidderInfos[string(bidderName)] = config.BidderInfo{ + ModifyingVastXmlAllowed: spec.ModifyingVastXmlAllowed, + OpenRTB: &config.OpenRTBInfo{Version: ortbVersion}, + } + } else { + bidderInfos[string(bidderName)] = config.BidderInfo{} } } for alias, coreBidder := range aliases { - if spec, ok := expectations[alias]; ok { + if spec, ok := exSpec.OutgoingRequests[alias]; ok { if bidder, ok := bidderAdapters[openrtb_ext.BidderName(coreBidder)]; ok { bidder.(*validatingBidder).expectations[alias] = spec.ExpectedRequest bidder.(*validatingBidder).mockResponses[alias] = spec.MockResponse @@ -2418,13 +2435,19 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } var hostSChainNode *openrtb2.SupplyChainNode - if hostSChainFlag { + if exSpec.HostSChainFlag { hostSChainNode = &openrtb2.SupplyChainNode{ ASI: "pbshostcompany.com", SID: "00001", RID: "BidRequest", HP: openrtb2.Int8Ptr(1), } } metricsEngine := metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil) + paramValidator, err := openrtb_ext.NewBidderParamsValidator("../static/bidder-params") + if err != nil { + t.Fatalf("Failed to create params validator: %v", error) + } + bidderMap := GetActiveBidders(bidderInfos) + requestValidator := ortb.NewRequestValidator(bidderMap, map[string]string{}, paramValidator) requestSplitter := requestSplitter{ bidderToSyncerKey: bidderToSyncerKey, me: metricsEngine, @@ -2432,6 +2455,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] gdprPermsBuilder: gdprPermsBuilder, hostSChainNode: hostSChainNode, bidderInfo: bidderInfos, + requestValidator: requestValidator, } return &exchange{ @@ -2449,39 +2473,43 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] externalURL: "http://localhost", bidIDGenerator: bidIDGenerator, hostSChainNode: hostSChainNode, - server: config.Server{ExternalUrl: server.ExternalUrl, GvlID: server.GvlID, DataCenter: server.DataCenter}, - bidValidationEnforcement: hostBidValidation, + server: config.Server{ExternalUrl: exSpec.Server.ExternalUrl, GvlID: exSpec.Server.GvlID, DataCenter: exSpec.Server.DataCenter}, + bidValidationEnforcement: exSpec.HostConfigBidValidation, requestSplitter: requestSplitter, - priceFloorEnabled: floorsFlag, + priceFloorEnabled: exSpec.FloorsEnabled, priceFloorFetcher: &mockPriceFloorFetcher{}, } } -type mockBidIDGenerator struct { +type fakeBidIDGenerator struct { GenerateBidID bool `json:"generateBidID"` ReturnError bool `json:"returnError"` + bidCount map[string]int } -func (big *mockBidIDGenerator) Enabled() bool { - return big.GenerateBidID +func (f *fakeBidIDGenerator) Enabled() bool { + return f.GenerateBidID } -func (big *mockBidIDGenerator) New() (string, error) { +func (f *fakeBidIDGenerator) New(bidder string) (string, error) { + if f.ReturnError { + return "", errors.New("Test error generating bid.ext.prebid.bidid") + } - if big.ReturnError { - err := errors.New("Test error generating bid.ext.prebid.bidid") - return "", err + if f.bidCount == nil { + f.bidCount = make(map[string]int) } - return "mock_uuid", nil + f.bidCount[bidder] += 1 + return fmt.Sprintf("bid-%v-%v", bidder, f.bidCount[bidder]), nil } -type fakeRandomDeduplicateBidBooleanGenerator struct { - returnValue bool +type fakeBooleanGenerator struct { + value bool } -func (m *fakeRandomDeduplicateBidBooleanGenerator) Generate() bool { - return m.returnValue +func (f *fakeBooleanGenerator) Generate() bool { + return f.value } func newExtRequest() openrtb_ext.ExtRequest { @@ -2566,10 +2594,10 @@ func TestCategoryMapping(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2583,7 +2611,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2621,10 +2649,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2638,7 +2666,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2675,9 +2703,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2690,7 +2718,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2757,9 +2785,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2772,7 +2800,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2849,8 +2877,8 @@ func TestCategoryDedupe(t *testing.T) { Currency: "USD", }, } - deduplicateGenerator := fakeRandomDeduplicateBidBooleanGenerator{returnValue: tt.dedupeGeneratorValue} - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &nonBids{}) + deduplicateGenerator := fakeBooleanGenerator{value: tt.dedupeGeneratorValue} + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &SeatNonBidBuilder{}) assert.Nil(t, err) assert.Equal(t, 3, len(rejections)) @@ -2885,11 +2913,11 @@ func TestNoCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_5 := entities.PbsOrtbBid{Bid: &bid5, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -2919,7 +2947,7 @@ func TestNoCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") @@ -2965,8 +2993,8 @@ func TestCategoryMappingBidderName(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -2984,7 +3012,7 @@ func TestCategoryMappingBidderName(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3019,8 +3047,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, nil, nil, 0, false, "", 12.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 12.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -3038,7 +3066,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3131,7 +3159,7 @@ func TestBidRejectionErrors(t *testing.T) { innerBids := []*entities.PbsOrtbBid{} for _, bid := range test.bids { currentBid := entities.PbsOrtbBid{ - bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, nil, 0, false, "", 10.0000, "USD", ""} + Bid: bid, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids = append(innerBids, ¤tBid) } @@ -3139,7 +3167,7 @@ func TestBidRejectionErrors(t *testing.T) { adapterBids[bidderName] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) if len(test.expectedCatDur) > 0 { // Bid deduplication case @@ -3179,8 +3207,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1 := entities.PbsOrtbBid{Bid: &bidApn1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn2 := entities.PbsOrtbBid{Bid: &bidApn2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1, @@ -3202,7 +3230,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - bidCategory, _, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, _, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Len(t, rejections, 1, "There should be 1 bid rejection message") @@ -3259,11 +3287,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1} bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} - bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn2_1 := entities.PbsOrtbBid{Bid: &bidApn2_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn2_2 := entities.PbsOrtbBid{Bid: &bidApn2_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1_1, @@ -3286,7 +3314,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}, &nonBids{}) + _, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") @@ -3341,9 +3369,9 @@ func TestRemoveBidById(t *testing.T) { bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_3 := entities.PbsOrtbBid{Bid: &bidApn1_3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} type aTest struct { desc string @@ -3544,7 +3572,7 @@ func TestApplyDealSupport(t *testing.T) { }, } - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.in.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.in.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} bidCategory := map[string]string{ bid.Bid.ID: test.in.targ["hb_pb_cat_dur"], } @@ -3604,8 +3632,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3650,8 +3678,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3701,8 +3729,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3893,7 +3921,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { } for _, test := range testCases { - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} bidCategory := map[string]string{ bid.Bid.ID: test.targ["hb_pb_cat_dur"], } @@ -4110,7 +4138,7 @@ func TestStoredAuctionResponses(t *testing.T) { e.cache = &wellBehavedCache{} e.me = &metricsConf.NilMetricsEngine{} e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.gdprPermsBuilder = fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -4132,7 +4160,7 @@ func TestStoredAuctionResponses(t *testing.T) { SeatBid: []openrtb2.SeatBid{ { Bid: []openrtb2.Bid{ - {ID: "bid_id", ImpID: "impression-id", Ext: json.RawMessage(`{"origbidcpm":0,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"video"}}`)}, + {ID: "bid_id", ImpID: "impression-id", Ext: json.RawMessage(`{"origbidcpm":0,"prebid":{"meta":{},"type":"video"}}`)}, }, Seat: "appnexus", }, @@ -4476,7 +4504,7 @@ func TestAuctionDebugEnabled(t *testing.T) { e.cache = &wellBehavedCache{} e.me = &metricsConf.NilMetricsEngine{} e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.gdprPermsBuilder = fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -4553,7 +4581,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) { }, }.Builder - e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange) + e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange) // Define mock incoming bid requeset mockBidRequest := &openrtb2.BidRequest{ @@ -4750,7 +4778,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids []*entities.PbsOrtbBid givenSeat openrtb_ext.BidderName expectedNumOfBids int - expectedNonBids *nonBids + expectedNonBids *SeatNonBidBuilder expectedNumDebugErrors int expectedNumDebugWarnings int }{ @@ -4758,18 +4786,16 @@ func TestMakeBidWithValidation(t *testing.T) { name: "One_of_two_bids_is_invalid_based_on_DSA_object_presence", givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), givenValidations: config.Validations{}, - givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {}}`)}}, {Bid: &openrtb2.Bid{}}}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - StatusCode: 300, - Ext: openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{}, - }, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + StatusCode: 300, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{}, }, }, }, @@ -4783,17 +4809,15 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - StatusCode: 351, - Ext: openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ - W: 200, - H: 200, - }, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + StatusCode: 351, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + W: 200, + H: 200, }, }, }, @@ -4808,7 +4832,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4817,12 +4841,15 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - ImpId: "1", - StatusCode: 352, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + ImpId: "1", + StatusCode: 352, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{}, + }, }, }, }, @@ -4835,7 +4862,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4844,7 +4871,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, }, { name: "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions", @@ -4852,7 +4879,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, }, } @@ -4901,7 +4928,7 @@ func TestMakeBidWithValidation(t *testing.T) { } e.bidValidationEnforcement = test.givenValidations sampleBids := test.givenBids - nonBids := &nonBids{} + nonBids := &SeatNonBidBuilder{} resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids) assert.Equal(t, 0, len(resultingErrs)) @@ -5077,7 +5104,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -5219,6 +5246,7 @@ func TestGetAllBids(t *testing.T) { ID: "1", }, OriginalBidCur: "USD", + AdapterCode: openrtb_ext.BidderPubmatic, }, }, Currency: "USD", @@ -5287,6 +5315,7 @@ func TestGetAllBids(t *testing.T) { ID: "1", }, OriginalBidCur: "USD", + AdapterCode: openrtb_ext.BidderPubmatic, }, }, Currency: "USD", @@ -5300,6 +5329,7 @@ func TestGetAllBids(t *testing.T) { ID: "2", }, OriginalBidCur: "USD", + AdapterCode: openrtb_ext.BidderPubmatic, }, }, Currency: "USD", @@ -5367,6 +5397,7 @@ func TestGetAllBids(t *testing.T) { ID: "2", }, OriginalBidCur: "USD", + AdapterCode: openrtb_ext.BidderPubmatic, }, }, Currency: "USD", @@ -5474,17 +5505,19 @@ type exchangeSpec struct { DebugLog *DebugLog `json:"debuglog,omitempty"` EventsEnabled bool `json:"events_enabled,omitempty"` StartTime int64 `json:"start_time_ms,omitempty"` - BidIDGenerator *mockBidIDGenerator `json:"bidIDGenerator,omitempty"` + BidIDGenerator *fakeBidIDGenerator `json:"bidIDGenerator,omitempty"` RequestType *metrics.RequestType `json:"requestType,omitempty"` PassthroughFlag bool `json:"passthrough_flag,omitempty"` HostSChainFlag bool `json:"host_schain_flag,omitempty"` HostConfigBidValidation config.Validations `json:"host_bid_validations"` AccountConfigBidValidation config.Validations `json:"account_bid_validations"` AccountFloorsEnabled bool `json:"account_floors_enabled"` + AccountEnforceDealFloors bool `json:"account_enforce_deal_floors"` FledgeEnabled bool `json:"fledge_enabled,omitempty"` MultiBid *multiBidSpec `json:"multiBid,omitempty"` Server exchangeServer `json:"server,omitempty"` - AccountPrivacy *config.AccountPrivacy `json:"accountPrivacy,omitempty"` + AccountPrivacy config.AccountPrivacy `json:"accountPrivacy,omitempty"` + ORTBVersion map[string]string `json:"ortbversion"` } type multiBidSpec struct { @@ -5539,9 +5572,10 @@ type bidderSeatBid struct { // bidderBid is basically a subset of entities.PbsOrtbBid from exchange/bidder.go. // See the comment on bidderSeatBid for more info. type bidderBid struct { - Bid *openrtb2.Bid `json:"ortbBid,omitempty"` - Type string `json:"bidType,omitempty"` - Meta *openrtb_ext.ExtBidPrebidMeta `json:"bidMeta,omitempty"` + Bid *openrtb2.Bid `json:"ortbBid,omitempty"` + Type string `json:"bidType,omitempty"` + BidVideo *openrtb_ext.ExtBidPrebidVideo `json:"bidVideo,omitempty"` + Meta *openrtb_ext.ExtBidPrebidMeta `json:"bidMeta,omitempty"` } type mockIdFetcher map[string]string @@ -5588,6 +5622,7 @@ func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderR bids[i] = &entities.PbsOrtbBid{ OriginalBidCPM: mockSeatBid.Bids[i].Bid.Price, Bid: mockSeatBid.Bids[i].Bid, + BidVideo: mockSeatBid.Bids[i].BidVideo, BidType: openrtb_ext.BidType(mockSeatBid.Bids[i].Type), BidMeta: mockSeatBid.Bids[i].Meta, } @@ -5770,6 +5805,24 @@ func (m *mockBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequ return args.Get(0).(*adapters.BidderResponse), args.Get(1).([]error) } +func parseRequestAliases(r openrtb2.BidRequest) (map[string]string, error) { + if len(r.Ext) == 0 { + return nil, nil + } + + ext := struct { + Prebid struct { + Aliases map[string]string `json:"aliases"` + } `json:"prebid"` + }{} + + if err := jsonutil.Unmarshal(r.Ext, &ext); err != nil { + return nil, err + } + + return ext.Prebid.Aliases, nil +} + func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, error) { bidRequest := req.BidRequest imp := bidRequest.Imp[0] @@ -6011,7 +6064,7 @@ func TestSelectNewDuration(t *testing.T) { func TestSetSeatNonBid(t *testing.T) { type args struct { bidResponseExt *openrtb_ext.ExtBidResponse - seatNonBids nonBids + seatNonBids SeatNonBidBuilder } tests := []struct { name string @@ -6020,12 +6073,12 @@ func TestSetSeatNonBid(t *testing.T) { }{ { name: "empty-seatNonBidsMap", - args: args{seatNonBids: nonBids{}, bidResponseExt: nil}, + args: args{seatNonBids: SeatNonBidBuilder{}, bidResponseExt: nil}, want: nil, }, { name: "nil-bidResponseExt", - args: args{seatNonBids: nonBids{seatNonBidsMap: map[string][]openrtb_ext.NonBid{"key": nil}}, bidResponseExt: nil}, + args: args{seatNonBids: SeatNonBidBuilder{"key": nil}, bidResponseExt: nil}, want: &openrtb_ext.ExtBidResponse{ Prebid: &openrtb_ext.ExtResponsePrebid{ SeatNonBid: []openrtb_ext.SeatNonBid{{ @@ -6297,3 +6350,11 @@ func TestBidsToUpdate(t *testing.T) { }) } } + +type mockRequestValidator struct { + errors []error +} + +func (mrv *mockRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, cfg ortb.ValidationConfig, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error { + return mrv.errors +} diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json index 14f1181e96a..cd5061001d4 100644 --- a/exchange/exchangetest/append-bidder-names.json +++ b/exchange/exchangetest/append-bidder-names.json @@ -149,10 +149,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "hb_pb_cat_dur": "0.20_VideoGames_30s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s_appnexus", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/ccpa-featureflag-on.json b/exchange/exchangetest/ccpa-featureflag-on.json index 0c452098da8..c6d37ed91e4 100644 --- a/exchange/exchangetest/ccpa-featureflag-on.json +++ b/exchange/exchangetest/ccpa-featureflag-on.json @@ -1,5 +1,8 @@ { "enforceCcpa": true, + "ortbversion": { + "appnexus":"2.6" + }, "incomingRequest": { "ortbRequest": { "id": "some-request-id", @@ -26,9 +29,7 @@ } ], "regs": { - "ext": { - "us_privacy": "1-Y-" - } + "us_privacy": "1-Y-" }, "user": { "buyeruid": "some-buyer-id" @@ -59,9 +60,7 @@ } ], "regs": { - "ext": { - "us_privacy": "1-Y-" - } + "us_privacy": "1-Y-" }, "user": {} } diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json index 5c0e029c975..8cd095c7bc4 100644 --- a/exchange/exchangetest/debuglog_disabled.json +++ b/exchange/exchangetest/debuglog_disabled.json @@ -158,10 +158,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json index 11a63f5db2e..6580dc8ad18 100644 --- a/exchange/exchangetest/debuglog_enabled.json +++ b/exchange/exchangetest/debuglog_enabled.json @@ -160,10 +160,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/eidpermissions-denied.json b/exchange/exchangetest/eidpermissions-denied.json index 72431595fe3..4f6a25e375d 100644 --- a/exchange/exchangetest/eidpermissions-denied.json +++ b/exchange/exchangetest/eidpermissions-denied.json @@ -6,18 +6,16 @@ "page": "test.somepage.com" }, "user": { - "ext": { - "eids": [ - { - "source": "source1", - "uids": [ - { - "id": "id1" - } - ] - } - ] - } + "eids": [ + { + "source": "source1", + "uids": [ + { + "id": "id1" + } + ] + } + ] }, "ext": { "prebid": { diff --git a/exchange/exchangetest/firstpartydata-multibidder-user-eids.json b/exchange/exchangetest/firstpartydata-multibidder-user-eids.json new file mode 100644 index 00000000000..f16907dcdb2 --- /dev/null +++ b/exchange/exchangetest/firstpartydata-multibidder-user-eids.json @@ -0,0 +1,239 @@ +{ + "requestType": "openrtb2-web", + "ortbversion": { + "appnexus":"2.6", + "rubicon":"2.6" + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "id": "reqUserID", + "keywords": "userKeyword!", + "eids": [ + { + "source": "reqeid1", + "uids": [ + { + "id": "reqeiduid1" + } + ] + }, + { + "source": "reqieid2", + "uids": [ + { + "id": "reqeiduid2" + } + ] + } + ] + }, + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + } + } + ], + "ext": { + "prebid": { + "data": { + "eidpermissions": [ + { + "source": "reqeid1", + "bidders": [ + "rubicon" + ] + } + ], + "bidders": [ + "appnexus", + "rubicon" + ] + }, + "bidderconfig": [ + { + "bidders": [ + "appnexus" + ], + "config": { + "ortb2": { + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + } + } + } + }, + { + "bidders": [ + "rubicon" + ], + "config": { + "ortb2": { + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords", + "eids": [ + { + "source": "fpdeid1", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + }, + { + "source": "fpdeid2", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + } + ] + } + } + } + } + ] + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ], + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + }, + "user": { + "id": "reqUserID", + "keywords": "userKeyword!", + "eids": [ + { + "source": "reqieid2", + "uids": [ + { + "id": "reqeiduid2" + } + ] + } + ] + } + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords", + "eids": [ + { + "source": "fpdeid1", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + }, + { + "source": "fpdeid2", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + } + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-user-eids-req-user-nil.json b/exchange/exchangetest/firstpartydata-user-eids-req-user-nil.json new file mode 100644 index 00000000000..2c3b47dc76e --- /dev/null +++ b/exchange/exchangetest/firstpartydata-user-eids-req-user-nil.json @@ -0,0 +1,203 @@ +{ + "requestType": "openrtb2-web", + "ortbversion": { + "appnexus":"2.6", + "rubicon":"2.6" + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + } + } + ], + "ext": { + "prebid": { + "data": { + "eidpermissions": [ + { + "source": "reqeid1", + "bidders": [ + "rubicon" + ] + } + ], + "bidders": [ + "appnexus", + "rubicon" + ] + }, + "bidderconfig": [ + { + "bidders": [ + "appnexus" + ], + "config": { + "ortb2": { + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + } + } + } + }, + { + "bidders": [ + "rubicon" + ], + "config": { + "ortb2": { + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords", + "eids": [ + { + "source": "fpdeid1", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + }, + { + "source": "fpdeid2", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + } + ] + } + } + } + } + ] + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ], + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + } + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords", + "eids": [ + { + "source": "fpdeid1", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + }, + { + "source": "fpdeid2", + "uids": [ + { + "id": "fpdeiduid1" + } + ] + } + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-user-nileids-req-user-nil.json b/exchange/exchangetest/firstpartydata-user-nileids-req-user-nil.json new file mode 100644 index 00000000000..3ea8382176c --- /dev/null +++ b/exchange/exchangetest/firstpartydata-user-nileids-req-user-nil.json @@ -0,0 +1,163 @@ +{ + "requestType": "openrtb2-web", + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + } + } + ], + "ext": { + "prebid": { + "data": { + "eidpermissions": [ + { + "source": "reqeid1", + "bidders": [ + "rubicon" + ] + } + ], + "bidders": [ + "appnexus", + "rubicon" + ] + }, + "bidderconfig": [ + { + "bidders": [ + "appnexus" + ], + "config": { + "ortb2": { + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + } + } + } + }, + { + "bidders": [ + "rubicon" + ], + "config": { + "ortb2": { + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords" + } + } + } + } + ] + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ], + "site": { + "domain": "fpd_appnexus_site_domain", + "page": "fpd_appnexus_site_page" + } + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "user": { + "id": "fpdSiteId!4", + "yob": 2000, + "keywords": "fpd keywords" + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/generate-bid-id-error.json b/exchange/exchangetest/generate-bid-id-error.json new file mode 100644 index 00000000000..3536f38b3d1 --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-error.json @@ -0,0 +1,203 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": true + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.20", + "hb_pb_appnexus": "0.20", + "hb_pb_cat_dur": "0.20_VideoGames_30s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s_appnexus", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" + } + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + } + }, + "errors": { + "prebid": [ + { + "code": 999, + "message": "Error generating bid.ext.prebid.bidid" + } + ] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/generate-bid-id-many.json b/exchange/exchangetest/generate-bid-id-many.json new file mode 100644 index 00000000000..ecdefbaa71b --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-many.json @@ -0,0 +1,170 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": false + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ] + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid-1", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30 + } + }, + { + "ortbBid": { + "id": "apn-bid-2", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid-1", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "bidid": "bid-appnexus-1", + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } + } + } + }, + { + "id": "apn-bid-2", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "bidid": "bid-appnexus-2", + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/generate-bid-id-one.json b/exchange/exchangetest/generate-bid-id-one.json new file mode 100644 index 00000000000..ccd6abd05f2 --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-one.json @@ -0,0 +1,129 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": false + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ] + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "primary_category": "" + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + }, + "bidid": "bid-appnexus-1", + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/include-brand-category.json b/exchange/exchangetest/include-brand-category.json index c0904448375..d3b5e87a8f8 100644 --- a/exchange/exchangetest/include-brand-category.json +++ b/exchange/exchangetest/include-brand-category.json @@ -147,10 +147,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/multi-bids-different-ortb-versions.json b/exchange/exchangetest/multi-bids-different-ortb-versions.json new file mode 100644 index 00000000000..37e7fc582c7 --- /dev/null +++ b/exchange/exchangetest/multi-bids-different-ortb-versions.json @@ -0,0 +1,276 @@ +{ + "requestType": "openrtb2-web", + "ortbversion": { + "appnexus":"2.6", + "rubicon":"2.5" + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "id": "my-user-id", + "buyeruid": "my-buyer-uid", + "geo": { + "lat": 123.456, + "lon": 678.90, + "zip": "90210" + }, + "consent": "AAAA", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ], + "ext": {} + }, + "imp": [ + { + "rwdd": 1, + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + } + } + } + ], + "source": { + "fd": 1, + "tid": "abc123", + "pchain": "tag_placement", + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "asi", + "sid": "sid", + "rid": "rid", + "ext": {} + } + ], + "ver": "ver", + "ext": {} + }, + "ext": {} + }, + "regs": { + "coppa": 1, + "gdpr": 1, + "us_privacy": "1YYY", + "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN", + "gppsid": [ + 6 + ] + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "geo": {}, + "consent": "AAAA", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ], + "ext": {} + }, + "imp": [ + { + "rwdd": 1, + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ], + "source": { + "fd": 1, + "tid": "abc123", + "pchain": "tag_placement", + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "asi", + "sid": "sid", + "rid": "rid", + "ext": {} + } + ], + "ver": "ver", + "ext": {} + }, + "ext": {} + }, + "regs": { + "coppa": 1, + "gdpr": 1, + "us_privacy": "1YYY", + "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN" + } + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { + "geo": {}, + "ext": { + "consent": "AAAA", + "eids": [ + { + "source": "source", + "uids": [ + { + "id": "1", + "atype": 1, + "ext": {} + }, + { + "id": "1", + "atype": 1, + "ext": {} + } + ], + "ext": {} + } + ] + } + }, + "imp": [ + { + "id": "some-imp-id", + "banner": { + "format": [ + { + "w": 600, + "h": 500 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "prebid": { + "is_rewarded_inventory":1 + } + } + } + ], + "source": { + "fd": 1, + "tid": "abc123", + "pchain": "tag_placement", + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "asi", + "sid": "sid", + "rid": "rid", + "ext": {} + } + ], + "ver": "ver", + "ext": {} + } + } + }, + "regs": { + "coppa": 1, + "gpp":"DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN", + "ext": { + "gdpr": 1, + "us_privacy": "1YYY" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/schain-host-and-request.json b/exchange/exchangetest/schain-host-and-request.json index 5bfcd1c3527..0552bb09b30 100644 --- a/exchange/exchangetest/schain-host-and-request.json +++ b/exchange/exchangetest/schain-host-and-request.json @@ -1,5 +1,8 @@ { "host_schain_flag": true, + "ortbversion": { + "appnexus":"2.6" + }, "incomingRequest": { "ortbRequest": { "id": "some-request-id", @@ -84,25 +87,23 @@ } }, "source": { - "ext": { - "schain": { - "complete": 1, - "nodes": [ - { - "asi": "whatever.com", - "sid": "1234", - "rid": "123-456-7890", - "hp": 1 - }, - { - "asi": "pbshostcompany.com", - "sid": "00001", - "rid": "BidRequest", - "hp": 1 - } - ], - "ver": "2.0" - } + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "whatever.com", + "sid": "1234", + "rid": "123-456-7890", + "hp": 1 + }, + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "2.0" } } }, diff --git a/exchange/exchangetest/schain-host-only.json b/exchange/exchangetest/schain-host-only.json index 0cc077e2fdb..89fb70c3676 100644 --- a/exchange/exchangetest/schain-host-only.json +++ b/exchange/exchangetest/schain-host-only.json @@ -1,5 +1,8 @@ { "host_schain_flag": true, + "ortbversion": { + "appnexus":"2.6" + }, "incomingRequest": { "ortbRequest": { "id": "some-request-id", @@ -64,19 +67,17 @@ } }, "source": { - "ext": { - "schain": { - "complete": 0, - "nodes": [ - { - "asi": "pbshostcompany.com", - "sid": "00001", - "rid": "BidRequest", - "hp": 1 - } - ], - "ver": "1.0" - } + "schain": { + "complete": 0, + "nodes": [ + { + "asi": "pbshostcompany.com", + "sid": "00001", + "rid": "BidRequest", + "hp": 1 + } + ], + "ver": "1.0" } } }, diff --git a/exchange/gdpr.go b/exchange/gdpr.go index 52fb860f5df..d548c8d7019 100644 --- a/exchange/gdpr.go +++ b/exchange/gdpr.go @@ -16,11 +16,11 @@ func getGDPR(req *openrtb_ext.RequestWrapper) (gdpr.Signal, error) { } return gdpr.SignalNo, nil } - re, err := req.GetRegExt() - if re == nil || re.GetGDPR() == nil || err != nil { - return gdpr.SignalAmbiguous, err + if req.Regs != nil && req.Regs.GDPR != nil { + return gdpr.IntSignalParse(int(*req.Regs.GDPR)) } - return gdpr.Signal(*re.GetGDPR()), nil + return gdpr.SignalAmbiguous, nil + } // getConsent will pull the consent string from an openrtb request @@ -29,9 +29,8 @@ func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (conse consent = gpp.Sections[i].GetValue() return } - ue, err := req.GetUserExt() - if ue == nil || ue.GetConsent() == nil || err != nil { - return + if req.User != nil { + return req.User.Consent, nil } - return *ue.GetConsent(), nil + return } diff --git a/exchange/gdpr_test.go b/exchange/gdpr_test.go index c73112be6f3..a775e0b1fa6 100644 --- a/exchange/gdpr_test.go +++ b/exchange/gdpr_test.go @@ -1,7 +1,6 @@ package exchange import ( - "encoding/json" "testing" gpplib "github.com/prebid/go-gpp" @@ -9,6 +8,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/gdpr" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -21,17 +21,17 @@ func TestGetGDPR(t *testing.T) { }{ { description: "Regs Ext GDPR = 0", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": 0}`)}, + giveRegs: &openrtb2.Regs{GDPR: ptrutil.ToPtr[int8](0)}, wantGDPR: gdpr.SignalNo, }, { description: "Regs Ext GDPR = 1", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": 1}`)}, + giveRegs: &openrtb2.Regs{GDPR: ptrutil.ToPtr[int8](1)}, wantGDPR: gdpr.SignalYes, }, { description: "Regs Ext GDPR = null", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": null}`)}, + giveRegs: &openrtb2.Regs{GDPR: nil}, wantGDPR: gdpr.SignalAmbiguous, }, { @@ -39,30 +39,19 @@ func TestGetGDPR(t *testing.T) { giveRegs: nil, wantGDPR: gdpr.SignalAmbiguous, }, - { - description: "Regs Ext is nil", - giveRegs: &openrtb2.Regs{Ext: nil}, - wantGDPR: gdpr.SignalAmbiguous, - }, - { - description: "JSON unmarshal error", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"`)}, - wantGDPR: gdpr.SignalAmbiguous, - wantError: true, - }, { description: "Regs Ext GDPR = null, GPPSID has tcf2", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": null}`), GPPSID: []int8{2}}, + giveRegs: &openrtb2.Regs{GDPR: nil, GPPSID: []int8{2}}, wantGDPR: gdpr.SignalYes, }, { description: "Regs Ext GDPR = 1, GPPSID has uspv1", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": 1}`), GPPSID: []int8{6}}, + giveRegs: &openrtb2.Regs{GDPR: ptrutil.ToPtr[int8](1), GPPSID: []int8{6}}, wantGDPR: gdpr.SignalNo, }, { description: "Regs Ext GDPR = 0, GPPSID has tcf2", - giveRegs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr": 0}`), GPPSID: []int8{2}}, + giveRegs: &openrtb2.Regs{GDPR: ptrutil.ToPtr[int8](0), GPPSID: []int8{2}}, wantGDPR: gdpr.SignalYes, }, { @@ -105,18 +94,13 @@ func TestGetConsent(t *testing.T) { wantError bool }{ { - description: "User Ext Consent is not empty", - giveUser: &openrtb2.User{Ext: json.RawMessage(`{"consent": "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}`)}, + description: "User Consent is not empty", + giveUser: &openrtb2.User{Consent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}, wantConsent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", }, { - description: "User Ext Consent is empty", - giveUser: &openrtb2.User{Ext: json.RawMessage(`{"consent": ""}`)}, - wantConsent: "", - }, - { - description: "User Ext is nil", - giveUser: &openrtb2.User{Ext: nil}, + description: "User Consent is empty", + giveUser: &openrtb2.User{Consent: ""}, wantConsent: "", }, { @@ -125,26 +109,20 @@ func TestGetConsent(t *testing.T) { wantConsent: "", }, { - description: "JSON unmarshal error", - giveUser: &openrtb2.User{Ext: json.RawMessage(`{`)}, - wantConsent: "", - wantError: true, - }, - { - description: "User Ext is nil, GPP has no GDPR", - giveUser: &openrtb2.User{Ext: nil}, + description: "User is nil, GPP has no GDPR", + giveUser: nil, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}}, wantConsent: "", }, { - description: "User Ext is nil, GPP has GDPR", - giveUser: &openrtb2.User{Ext: nil}, + description: "User is nil, GPP has GDPR", + giveUser: nil, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{2}, Sections: []gpplib.Section{&tcf1Section}}, wantConsent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", }, { - description: "User Ext has GDPR, GPP has GDPR", - giveUser: &openrtb2.User{Ext: json.RawMessage(`{"consent": "BSOMECONSENT"}`)}, + description: "User has GDPR, GPP has GDPR", + giveUser: &openrtb2.User{Consent: "BSOMECONSENT"}, giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{2}, Sections: []gpplib.Section{&tcf1Section}}, wantConsent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", }, diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go index 2dff2dfbcbf..f12c01a2af9 100644 --- a/exchange/non_bid_reason.go +++ b/exchange/non_bid_reason.go @@ -1,27 +1,55 @@ package exchange +import ( + "errors" + "net" + "syscall" + + "github.com/prebid/prebid-server/v2/errortypes" +) + // SeatNonBid list the reasons why bid was not resulted in positive bid // reason could be either No bid, Error, Request rejection or Response rejection -// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md -type NonBidReason int +// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md#list-non-bid-status-codes +type NonBidReason int64 const ( - NoBidUnknownError NonBidReason = 0 // No Bid - General + ErrorGeneral NonBidReason = 100 // Error - General + ErrorTimeout NonBidReason = 101 // Error - Timeout + ErrorBidderUnreachable NonBidReason = 103 // Error - Bidder Unreachable ResponseRejectedGeneral NonBidReason = 300 ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed) ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) ) -// Ptr returns pointer to own value. -func (n NonBidReason) Ptr() *NonBidReason { - return &n +func errorToNonBidReason(err error) NonBidReason { + switch errortypes.ReadCode(err) { + case errortypes.TimeoutErrorCode: + return ErrorTimeout + default: + return ErrorGeneral + } } -// Val safely dereferences pointer, returning default value (NoBidUnknownError) for nil. -func (n *NonBidReason) Val() NonBidReason { - if n == nil { - return NoBidUnknownError +// httpInfoToNonBidReason determines NoBidReason code (NBR) +// It will first try to resolve the NBR based on prebid's proprietary error code. +// If proprietary error code not found then it will try to determine NBR using +// system call level error code +func httpInfoToNonBidReason(httpInfo *httpCallInfo) NonBidReason { + nonBidReason := errorToNonBidReason(httpInfo.err) + if nonBidReason != ErrorGeneral { + return nonBidReason } - return *n + if isBidderUnreachableError(httpInfo) { + return ErrorBidderUnreachable + } + return ErrorGeneral +} + +// isBidderUnreachableError checks if the error is due to connection refused or no such host +func isBidderUnreachableError(httpInfo *httpCallInfo) bool { + var dnsErr *net.DNSError + isNoSuchHost := errors.As(httpInfo.err, &dnsErr) && dnsErr.IsNotFound + return errors.Is(httpInfo.err, syscall.ECONNREFUSED) || isNoSuchHost } diff --git a/exchange/non_bid_reason_test.go b/exchange/non_bid_reason_test.go new file mode 100644 index 00000000000..ab5c9b4f957 --- /dev/null +++ b/exchange/non_bid_reason_test.go @@ -0,0 +1,65 @@ +package exchange + +import ( + "errors" + "net" + "syscall" + "testing" + + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/stretchr/testify/assert" +) + +func Test_httpInfoToNonBidReason(t *testing.T) { + type args struct { + httpInfo *httpCallInfo + } + tests := []struct { + name string + args args + want NonBidReason + }{ + { + name: "error-timeout", + args: args{ + httpInfo: &httpCallInfo{ + err: &errortypes.Timeout{}, + }, + }, + want: ErrorTimeout, + }, + { + name: "error-general", + args: args{ + httpInfo: &httpCallInfo{ + err: errors.New("some_error"), + }, + }, + want: ErrorGeneral, + }, + { + name: "error-bidderUnreachable", + args: args{ + httpInfo: &httpCallInfo{ + err: syscall.ECONNREFUSED, + }, + }, + want: ErrorBidderUnreachable, + }, + { + name: "error-biddersUnreachable-no-such-host", + args: args{ + httpInfo: &httpCallInfo{ + err: &net.DNSError{IsNotFound: true}, + }, + }, + want: ErrorBidderUnreachable, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := httpInfoToNonBidReason(tt.args.httpInfo) + assert.Equal(t, tt.want, actual) + }) + } +} diff --git a/exchange/seat_non_bids.go b/exchange/seat_non_bids.go index 78c1b23e3f3..760431ef44f 100644 --- a/exchange/seat_non_bids.go +++ b/exchange/seat_non_bids.go @@ -5,22 +5,18 @@ import ( "github.com/prebid/prebid-server/v2/openrtb_ext" ) -type nonBids struct { - seatNonBidsMap map[string][]openrtb_ext.NonBid -} +type SeatNonBidBuilder map[string][]openrtb_ext.NonBid -// addBid is not thread safe as we are initializing and writing to map -func (snb *nonBids) addBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { - if bid == nil || bid.Bid == nil { +// rejectBid appends a non bid object to the builder based on a bid +func (b SeatNonBidBuilder) rejectBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { + if b == nil || bid == nil || bid.Bid == nil { return } - if snb.seatNonBidsMap == nil { - snb.seatNonBidsMap = make(map[string][]openrtb_ext.NonBid) - } + nonBid := openrtb_ext.NonBid{ ImpId: bid.Bid.ImpID, StatusCode: nonBidReason, - Ext: openrtb_ext.NonBidExt{ + Ext: &openrtb_ext.NonBidExt{ Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{ Price: bid.Bid.Price, ADomain: bid.Bid.ADomain, @@ -36,16 +32,29 @@ func (snb *nonBids) addBid(bid *entities.PbsOrtbBid, nonBidReason int, seat stri }}, }, } - - snb.seatNonBidsMap[seat] = append(snb.seatNonBidsMap[seat], nonBid) + b[seat] = append(b[seat], nonBid) } -func (snb *nonBids) get() []openrtb_ext.SeatNonBid { - if snb == nil { - return nil +// rejectImps appends a non bid object to the builder for every specified imp +func (b SeatNonBidBuilder) rejectImps(impIds []string, nonBidReason NonBidReason, seat string) { + nonBids := []openrtb_ext.NonBid{} + for _, impId := range impIds { + nonBid := openrtb_ext.NonBid{ + ImpId: impId, + StatusCode: int(nonBidReason), + } + nonBids = append(nonBids, nonBid) } - var seatNonBid []openrtb_ext.SeatNonBid - for seat, nonBids := range snb.seatNonBidsMap { + + if len(nonBids) > 0 { + b[seat] = append(b[seat], nonBids...) + } +} + +// slice transforms the seat non bid map into a slice of SeatNonBid objects representing the non-bids for each seat +func (b SeatNonBidBuilder) Slice() []openrtb_ext.SeatNonBid { + seatNonBid := make([]openrtb_ext.SeatNonBid, 0) + for seat, nonBids := range b { seatNonBid = append(seatNonBid, openrtb_ext.SeatNonBid{ Seat: seat, NonBid: nonBids, @@ -53,3 +62,16 @@ func (snb *nonBids) get() []openrtb_ext.SeatNonBid { } return seatNonBid } + +// append adds the nonBids from the input nonBids to the current nonBids. +// This method is not thread safe as we are initializing and writing to map +func (b SeatNonBidBuilder) append(nonBids ...SeatNonBidBuilder) { + if b == nil { + return + } + for _, nonBid := range nonBids { + for seat, nonBids := range nonBid { + b[seat] = append(b[seat], nonBids...) + } + } +} diff --git a/exchange/seat_non_bids_test.go b/exchange/seat_non_bids_test.go index 103c0939496..b754f885965 100644 --- a/exchange/seat_non_bids_test.go +++ b/exchange/seat_non_bids_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSeatNonBidsAdd(t *testing.T) { +func TestRejectBid(t *testing.T) { type fields struct { - seatNonBidsMap map[string][]openrtb_ext.NonBid + builder SeatNonBidBuilder } type args struct { bid *entities.PbsOrtbBid @@ -22,89 +22,512 @@ func TestSeatNonBidsAdd(t *testing.T) { name string fields fields args args - want map[string][]openrtb_ext.NonBid + want SeatNonBidBuilder }{ { - name: "nil-seatNonBidsMap", - fields: fields{seatNonBidsMap: nil}, - args: args{}, - want: nil, + name: "nil_builder", + fields: fields{ + builder: nil, + }, + args: args{}, + want: nil, }, { - name: "nil-seatNonBidsMap-with-bid-object", - fields: fields{seatNonBidsMap: nil}, - args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder1"}, - want: sampleSeatNonBidMap("bidder1", 1), + name: "nil_pbsortbid", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: nil, + }, + want: SeatNonBidBuilder{}, }, { - name: "multiple-nonbids-for-same-seat", - fields: fields{seatNonBidsMap: sampleSeatNonBidMap("bidder2", 1)}, - args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder2"}, - want: sampleSeatNonBidMap("bidder2", 2), + name: "nil_bid", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: nil, + }, + }, + want: SeatNonBidBuilder{}, + }, + { + name: "append_nonbids_new_seat", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp1", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat1", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, + }, + { + name: "append_nonbids_for_different_seat", + fields: fields{ + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + }, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp2", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat2", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + "seat2": []openrtb_ext.NonBid{ + { + ImpId: "Imp2", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, + }, + { + name: "append_nonbids_for_existing_seat", + fields: fields{ + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + }, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp2", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat1", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + { + ImpId: "Imp2", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - snb := &nonBids{ - seatNonBidsMap: tt.fields.seatNonBidsMap, - } - snb.addBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) - assert.Equalf(t, tt.want, snb.seatNonBidsMap, "expected seatNonBidsMap not nil") + snb := tt.fields.builder + snb.rejectBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) + assert.Equal(t, tt.want, snb) }) } } -func TestSeatNonBidsGet(t *testing.T) { - type fields struct { - snb *nonBids - } +func TestAppend(t *testing.T) { tests := []struct { - name string - fields fields - want []openrtb_ext.SeatNonBid + name string + builder SeatNonBidBuilder + toAppend []SeatNonBidBuilder + expected SeatNonBidBuilder }{ { - name: "get-seat-nonbids", - fields: fields{&nonBids{sampleSeatNonBidMap("bidder1", 2)}}, - want: sampleSeatBids("bidder1", 2), + name: "nil_buider", + builder: nil, + toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, + expected: nil, + }, + { + name: "empty_builder", + builder: SeatNonBidBuilder{}, + toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + }, + { + name: "append_one_different_seat", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, + }, + { + name: "append_multiple_different_seats", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, {"seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}, "seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}, + }, + { + name: "nil_append", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: nil, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + }, + { + name: "empty_append", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, }, { - name: "nil-seat-nonbids", - fields: fields{nil}, + name: "append_multiple_same_seat", + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp1"}, + }, + }, + toAppend: []SeatNonBidBuilder{ + { + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp2"}, + }, + }, + }, + expected: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp1"}, + {ImpId: "imp2"}, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.fields.snb.get(); !assert.Equal(t, tt.want, got) { - t.Errorf("seatNonBids.get() = %v, want %v", got, tt.want) - } + tt.builder.append(tt.toAppend...) + assert.Equal(t, tt.expected, tt.builder) }) } } -var sampleSeatNonBidMap = func(seat string, nonBidCount int) map[string][]openrtb_ext.NonBid { - nonBids := make([]openrtb_ext.NonBid, 0) - for i := 0; i < nonBidCount; i++ { - nonBids = append(nonBids, openrtb_ext.NonBid{ - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, - }) +func TestRejectImps(t *testing.T) { + tests := []struct { + name string + impIDs []string + builder SeatNonBidBuilder + want SeatNonBidBuilder + }{ + { + name: "nil_imps", + impIDs: nil, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{}, + }, + { + name: "empty_imps", + impIDs: []string{}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{}, + }, + { + name: "one_imp", + impIDs: []string{"imp1"}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps_appended_to_prepopulated_list", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{ + "seat0": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 0, + }, + }, + }, + want: SeatNonBidBuilder{ + "seat0": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 0, + }, + }, + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps_appended_to_prepopulated_list_same_seat", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 300, + }, + }, + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 300, + }, + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, } - return map[string][]openrtb_ext.NonBid{ - seat: nonBids, + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.builder.rejectImps(test.impIDs, 300, "seat1") + + assert.Equal(t, len(test.builder), len(test.want)) + for seat := range test.want { + assert.ElementsMatch(t, test.want[seat], test.builder[seat]) + } + }) } } -var sampleSeatBids = func(seat string, nonBidCount int) []openrtb_ext.SeatNonBid { - seatNonBids := make([]openrtb_ext.SeatNonBid, 0) - seatNonBid := openrtb_ext.SeatNonBid{ - Seat: seat, - NonBid: make([]openrtb_ext.NonBid, 0), +func TestSlice(t *testing.T) { + tests := []struct { + name string + builder SeatNonBidBuilder + want []openrtb_ext.SeatNonBid + }{ + { + name: "nil", + builder: nil, + want: []openrtb_ext.SeatNonBid{}, + }, + { + name: "empty", + builder: SeatNonBidBuilder{}, + want: []openrtb_ext.SeatNonBid{}, + }, + { + name: "one_no_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{}, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "a", + }, + }, + }, + { + name: "one_with_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + Seat: "a", + }, + }, + }, + { + name: "many_no_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{}, + "b": []openrtb_ext.NonBid{}, + "c": []openrtb_ext.NonBid{}, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "a", + }, + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "b", + }, + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "c", + }, + }, + }, + { + name: "many_with_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + "b": []openrtb_ext.NonBid{ + { + ImpId: "imp3", + StatusCode: 300, + }, + }, + "c": []openrtb_ext.NonBid{ + { + ImpId: "imp4", + StatusCode: 400, + }, + { + ImpId: "imp5", + StatusCode: 500, + }, + }, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + Seat: "a", + }, + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp3", + StatusCode: 300, + }, + }, + Seat: "b", + }, + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp4", + StatusCode: 400, + }, + { + ImpId: "imp5", + StatusCode: 500, + }, + }, + Seat: "c", + }, + }, + }, } - for i := 0; i < nonBidCount; i++ { - seatNonBid.NonBid = append(seatNonBid.NonBid, openrtb_ext.NonBid{ - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.builder.Slice() + assert.ElementsMatch(t, test.want, result) }) } - seatNonBids = append(seatNonBids, seatNonBid) - return seatNonBids } diff --git a/exchange/utils.go b/exchange/utils.go index 676c015ae0e..5e3feab15f5 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -1,7 +1,6 @@ package exchange import ( - "bytes" "context" "encoding/json" "errors" @@ -9,9 +8,6 @@ import ( "math/rand" "strings" - "github.com/prebid/prebid-server/v2/ortb" - - "github.com/buger/jsonparser" "github.com/prebid/go-gdpr/vendorconsent" gpplib "github.com/prebid/go-gpp" gppConstants "github.com/prebid/go-gpp/constants" @@ -23,6 +19,7 @@ import ( "github.com/prebid/prebid-server/v2/gdpr" "github.com/prebid/prebid-server/v2/metrics" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" "github.com/prebid/prebid-server/v2/privacy/ccpa" "github.com/prebid/prebid-server/v2/privacy/lmt" @@ -32,6 +29,8 @@ import ( "github.com/prebid/prebid-server/v2/util/ptrutil" ) +var errInvalidRequestExt = errors.New("request.ext is invalid") + var channelTypeMap = map[metrics.RequestType]config.ChannelType{ metrics.ReqTypeAMP: config.ChannelAMP, metrics.ReqTypeORTB2App: config.ChannelApp, @@ -49,6 +48,7 @@ type requestSplitter struct { gdprPermsBuilder gdpr.PermissionsBuilder hostSChainNode *openrtb2.SupplyChainNode bidderInfo config.BidderInfos + requestValidator ortb.RequestValidator } // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: @@ -59,35 +59,51 @@ type requestSplitter struct { func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, auctionReq AuctionRequest, requestExt *openrtb_ext.ExtRequest, - gdprDefaultValue gdpr.Signal, bidAdjustmentFactors map[string]float64, -) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) { + gdprSignal gdpr.Signal, + gdprEnforced bool, + bidAdjustmentFactors map[string]float64, +) (bidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) { req := auctionReq.BidRequestWrapper - aliases, errs := parseAliases(req.BidRequest) - if len(errs) > 0 { + if err := PreloadExts(req); err != nil { return } - allowedBidderRequests = make([]BidderRequest, 0) + requestAliases, requestAliasesGVLIDs, errs := getRequestAliases(req) + if len(errs) > 0 { + return + } bidderImpWithBidResp := stored_responses.InitStoredBidResponses(req.BidRequest, auctionReq.StoredBidResponses) + hasStoredAuctionResponses := len(auctionReq.StoredAuctionResponses) > 0 - impsByBidder, err := splitImps(req.BidRequest.Imp) + impsByBidder, err := splitImps(req.BidRequest.Imp, rs.requestValidator, requestAliases, hasStoredAuctionResponses, auctionReq.StoredBidResponses) if err != nil { errs = []error{err} return } - aliasesGVLIDs, errs := parseAliasesGVLIDs(req.BidRequest) - if len(errs) > 0 { + explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User) + if err != nil { + errs = []error{err} return } + lowerCaseExplicitBuyerUIDs := make(map[string]string) + for bidder, uid := range explicitBuyerUIDs { + lowerKey := strings.ToLower(bidder) + lowerCaseExplicitBuyerUIDs[lowerKey] = uid + } - var allBidderRequests []BidderRequest - allBidderRequests, errs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, aliases, rs.hostSChainNode) + bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) + if err != nil { + errs = []error{err} + return + } - bidderNameToBidderReq := buildBidResponseRequest(req.BidRequest, bidderImpWithBidResp, aliases, auctionReq.BidderImpReplaceImpID) - //this function should be executed after getAuctionBidderRequests - allBidderRequests = mergeBidderRequests(allBidderRequests, bidderNameToBidderReq) + sChainWriter, err := schain.NewSChainWriter(requestExt, rs.hostSChainNode) + if err != nil { + errs = []error{err} + return + } var gpp gpplib.GppContainer if req.BidRequest.Regs != nil && len(req.BidRequest.Regs.GPP) > 0 { @@ -98,23 +114,12 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, } } - if auctionReq.Account.PriceFloors.IsAdjustForBidAdjustmentEnabled() { - //Apply BidAdjustmentFactor to imp.BidFloor - applyBidAdjustmentToFloor(allBidderRequests, bidAdjustmentFactors) - } - - gdprSignal, err := getGDPR(req) - if err != nil { - errs = append(errs, err) - } - consent, err := getConsent(req, gpp) if err != nil { errs = append(errs, err) } - gdprApplies := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && gdprDefaultValue == gdpr.SignalYes) - ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, aliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp) + ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, requestAliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp) if err != nil { errs = append(errs, err) } @@ -130,13 +135,8 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, privacyLabels.COPPAEnforced = coppa privacyLabels.LMTEnforced = lmt - var gdprEnforced bool var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{} - if gdprApplies { - gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType]) - } - if gdprEnforced { privacyLabels.GDPREnforced = true parsedConsent, err := vendorconsent.ParseString(consent) @@ -146,7 +146,7 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, } gdprRequestInfo := gdpr.RequestInfo{ - AliasGVLIDs: aliasesGVLIDs, + AliasGVLIDs: requestAliasesGVLIDs, Consent: consent, GDPRSignal: gdprSignal, PublisherID: auctionReq.LegacyLabels.PubID, @@ -154,92 +154,270 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } - // bidder level privacy policies - for _, bidderRequest := range allBidderRequests { - // fetchBids activity - scopedName := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderRequest.BidderName.String()} - fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids, scopedName, privacy.NewRequestFromBidRequest(*req)) - if !fetchBidsActivityAllowed { - // skip the call to a bidder if fetchBids activity is not allowed - // do not add this bidder to allowedBidderRequests + bidderRequests = make([]BidderRequest, 0, len(impsByBidder)) + + for bidder, imps := range impsByBidder { + fpdUserEIDsPresent := fpdUserEIDExists(req, auctionReq.FirstPartyData, bidder) + reqWrapperCopy := req.CloneAndClearImpWrappers() + bidRequestCopy := *req.BidRequest + reqWrapperCopy.BidRequest = &bidRequestCopy + reqWrapperCopy.Imp = imps + + coreBidder, isRequestAlias := resolveBidder(bidder, requestAliases) + + // apply bidder-specific schains + sChainWriter.Write(reqWrapperCopy, bidder) + + // eid scrubbing + if err := removeUnpermissionedEids(reqWrapperCopy, bidder); err != nil { + errs = append(errs, fmt.Errorf("unable to enforce request.ext.prebid.data.eidpermissions because %v", err)) continue } - var auctionPermissions gdpr.AuctionPermissions - var gdprErr error + // generate bidder-specific request ext + err = buildRequestExtForBidder(bidder, reqWrapperCopy, bidderParamsInReqExt, auctionReq.Account.AlternateBidderCodes) + if err != nil { + errs = append(errs, err) + continue + } - if gdprEnforced { - auctionPermissions, gdprErr = gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName) - if !auctionPermissions.AllowBidRequest { - // auction request is not permitted by GDPR - // do not add this bidder to allowedBidderRequests - rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName) - continue - } + // apply bid adjustments + if auctionReq.Account.PriceFloors.IsAdjustForBidAdjustmentEnabled() { + applyBidAdjustmentToFloor(reqWrapperCopy, bidder, bidAdjustmentFactors) } - ipConf := privacy.IPConf{IPV6: auctionReq.Account.Privacy.IPv6Config, IPV4: auctionReq.Account.Privacy.IPv4Config} + // prepare user + syncerKey := rs.bidderToSyncerKey[string(coreBidder)] + hadSync := prepareUser(reqWrapperCopy, bidder, syncerKey, lowerCaseExplicitBuyerUIDs, auctionReq.UserSyncs) - // FPD should be applied before policies, otherwise it overrides policies and activities restricted data - applyFPD(auctionReq.FirstPartyData, bidderRequest) + auctionPermissions := gdprPerms.AuctionActivitiesAllowed(ctx, coreBidder, openrtb_ext.BidderName(bidder)) - reqWrapper := &openrtb_ext.RequestWrapper{ - BidRequest: ortb.CloneBidRequestPartial(bidderRequest.BidRequest), + // privacy blocking + if rs.isBidderBlockedByPrivacy(reqWrapperCopy, auctionReq.Activities, auctionPermissions, coreBidder, openrtb_ext.BidderName(bidder)) { + continue } - passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName, privacy.NewRequestFromBidRequest(*req)) - if !passIDActivityAllowed { - //UFPD - privacy.ScrubUserFPD(reqWrapper) - } else { - // run existing policies (GDPR, CCPA, COPPA, LMT) - // potentially block passing IDs based on GDPR - if gdprEnforced && (gdprErr != nil || !auctionPermissions.PassID) { - privacy.ScrubGdprID(reqWrapper) - } - // potentially block passing IDs based on CCPA - if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) { - privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false) + // fpd + applyFPD(auctionReq.FirstPartyData, coreBidder, openrtb_ext.BidderName(bidder), isRequestAlias, reqWrapperCopy, fpdUserEIDsPresent) + + // privacy scrubbing + if err := rs.applyPrivacy(reqWrapperCopy, coreBidder, bidder, auctionReq, auctionPermissions, ccpaEnforcer, lmt, coppa); err != nil { + errs = append(errs, err) + continue + } + + // GPP downgrade: always downgrade unless we can confirm GPP is supported + if shouldSetLegacyPrivacy(rs.bidderInfo, string(coreBidder)) { + setLegacyGDPRFromGPP(reqWrapperCopy, gpp) + setLegacyUSPFromGPP(reqWrapperCopy, gpp) + } + + // remove imps with stored responses so they aren't sent to the bidder + if impResponses, ok := bidderImpWithBidResp[openrtb_ext.BidderName(bidder)]; ok { + removeImpsWithStoredResponses(reqWrapperCopy, impResponses) + } + + // down convert + info, ok := rs.bidderInfo[bidder] + if !ok || info.OpenRTB == nil || info.OpenRTB.Version != "2.6" { + reqWrapperCopy.Regs = ortb.CloneRegs(reqWrapperCopy.Regs) + if err := openrtb_ext.ConvertDownTo25(reqWrapperCopy); err != nil { + errs = append(errs, err) + continue } } - passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scopedName, privacy.NewRequestFromBidRequest(*req)) - if !passGeoActivityAllowed { - privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf) + // sync wrapper + if err := reqWrapperCopy.RebuildRequest(); err != nil { + errs = append(errs, err) + continue + } + + // choose labels + bidderLabels := metrics.AdapterLabels{ + Adapter: coreBidder, + } + if !hadSync && req.BidRequest.App == nil { + bidderLabels.CookieFlag = metrics.CookieFlagNo } else { - // run existing policies (GDPR, CCPA, COPPA, LMT) - // potentially block passing geo based on GDPR - if gdprEnforced && (gdprErr != nil || !auctionPermissions.PassGeo) { - privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf) - } - // potentially block passing geo based on CCPA - if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) { - privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false) - } + bidderLabels.CookieFlag = metrics.CookieFlagYes + } + if len(reqWrapperCopy.Imp) > 0 { + bidderLabels.Source = auctionReq.LegacyLabels.Source + bidderLabels.RType = auctionReq.LegacyLabels.RType + bidderLabels.PubID = auctionReq.LegacyLabels.PubID + bidderLabels.CookieFlag = auctionReq.LegacyLabels.CookieFlag + bidderLabels.AdapterBids = metrics.AdapterBidPresent } - if lmt || coppa { - privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa) + bidderRequest := BidderRequest{ + BidderName: openrtb_ext.BidderName(bidder), + BidderCoreName: coreBidder, + BidRequest: reqWrapperCopy.BidRequest, + IsRequestAlias: isRequestAlias, + BidderStoredResponses: bidderImpWithBidResp[openrtb_ext.BidderName(bidder)], + ImpReplaceImpId: auctionReq.BidderImpReplaceImpID[bidder], + BidderLabels: bidderLabels, } + bidderRequests = append(bidderRequests, bidderRequest) + } - passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scopedName, privacy.NewRequestFromBidRequest(*req)) - if !passTIDAllowed { - privacy.ScrubTID(reqWrapper) + return +} + +// fpdUserEIDExists determines if req fpd config had User.EIDs +func fpdUserEIDExists(req *openrtb_ext.RequestWrapper, fpd map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData, bidder string) bool { + fpdToApply, exists := fpd[openrtb_ext.BidderName(bidder)] + if !exists || fpdToApply == nil { + return false + } + if fpdToApply.User == nil { + return false + } + fpdUserEIDs := fpdToApply.User.EIDs + + if len(fpdUserEIDs) == 0 { + return false + } + if req.User == nil { + return true + } + + reqUserEIDs := req.User.EIDs + + if len(reqUserEIDs) != len(fpdUserEIDs) { + return true + } + + // if bidder fpd didn't have user.eids then user.eids will remain the same + // hence we can use the same index to compare elements + for i := range reqUserEIDs { + pReqUserEID := &reqUserEIDs[i] + pFpdUserEID := &fpdUserEIDs[i] + if pReqUserEID != pFpdUserEID { + return true } + } + return false +} - reqWrapper.RebuildRequest() - bidderRequest.BidRequest = reqWrapper.BidRequest +// removeImpsWithStoredResponses deletes imps with stored bid resp +func removeImpsWithStoredResponses(req *openrtb_ext.RequestWrapper, impBidResponses map[string]json.RawMessage) { + if len(impBidResponses) == 0 { + return + } - allowedBidderRequests = append(allowedBidderRequests, bidderRequest) + imps := req.Imp + req.Imp = nil //to indicate this bidder doesn't have real requests + for _, imp := range imps { + if _, ok := impBidResponses[imp.ID]; !ok { + //add real imp back to request + req.Imp = append(req.Imp, imp) + } + } +} - // GPP downgrade: always downgrade unless we can confirm GPP is supported - if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) { - setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp) - setLegacyUSPFromGPP(bidderRequest.BidRequest, gpp) +// PreloadExts ensures all exts have been unmarshalled into wrapper ext objects +func PreloadExts(req *openrtb_ext.RequestWrapper) error { + if req == nil { + return nil + } + if _, err := req.GetRequestExt(); err != nil { + return err + } + if _, err := req.GetUserExt(); err != nil { + return err + } + if _, err := req.GetDeviceExt(); err != nil { + return err + } + if _, err := req.GetRegExt(); err != nil { + return err + } + if _, err := req.GetSiteExt(); err != nil { + return err + } + if _, err := req.GetDOOHExt(); err != nil { + return err + } + if _, err := req.GetSourceExt(); err != nil { + return err + } + return nil +} + +func (rs *requestSplitter) isBidderBlockedByPrivacy(r *openrtb_ext.RequestWrapper, activities privacy.ActivityControl, auctionPermissions gdpr.AuctionPermissions, coreBidder, bidderName openrtb_ext.BidderName) bool { + // activities control + scope := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName.String()} + fetchBidsActivityAllowed := activities.Allow(privacy.ActivityFetchBids, scope, privacy.NewRequestFromBidRequest(*r)) + if !fetchBidsActivityAllowed { + return true + } + + // gdpr + if !auctionPermissions.AllowBidRequest { + rs.me.RecordAdapterGDPRRequestBlocked(coreBidder) + return true + } + + return false +} + +func (rs *requestSplitter) applyPrivacy(reqWrapper *openrtb_ext.RequestWrapper, coreBidderName openrtb_ext.BidderName, bidderName string, auctionReq AuctionRequest, auctionPermissions gdpr.AuctionPermissions, ccpaEnforcer privacy.PolicyEnforcer, lmt bool, coppa bool) error { + scope := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName} + ipConf := privacy.IPConf{IPV6: auctionReq.Account.Privacy.IPv6Config, IPV4: auctionReq.Account.Privacy.IPv4Config} + + bidRequest := ortb.CloneBidRequestPartial(reqWrapper.BidRequest) + reqWrapper.BidRequest = bidRequest + + passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scope, privacy.NewRequestFromBidRequest(*reqWrapper)) + buyerUIDSet := reqWrapper.User != nil && reqWrapper.User.BuyerUID != "" + buyerUIDRemoved := false + if !passIDActivityAllowed { + privacy.ScrubUserFPD(reqWrapper) + buyerUIDRemoved = true + } else { + if !auctionPermissions.PassID { + privacy.ScrubGdprID(reqWrapper) + buyerUIDRemoved = true + } + + if ccpaEnforcer.ShouldEnforce(bidderName) { + privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false) + buyerUIDRemoved = true } } + if buyerUIDSet && buyerUIDRemoved { + rs.me.RecordAdapterBuyerUIDScrubbed(coreBidderName) + } - return + passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scope, privacy.NewRequestFromBidRequest(*reqWrapper)) + if !passGeoActivityAllowed { + privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf) + } else { + if !auctionPermissions.PassGeo { + privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf) + } + if ccpaEnforcer.ShouldEnforce(bidderName) { + privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false) + } + } + + if lmt || coppa { + privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa) + } + + passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scope, privacy.NewRequestFromBidRequest(*reqWrapper)) + if !passTIDAllowed { + privacy.ScrubTID(reqWrapper) + } + + if err := reqWrapper.RebuildRequest(); err != nil { + return err + } + + // *bidRequest = *reqWrapper.BidRequest + return nil } func shouldSetLegacyPrivacy(bidderInfo config.BidderInfos, bidder string) bool { @@ -259,14 +437,14 @@ func ccpaEnabled(account *config.Account, privacyConfig config.Privacy, requestT return privacyConfig.CCPA.Enforce } -func extractCCPA(orig *openrtb2.BidRequest, privacyConfig config.Privacy, account *config.Account, aliases map[string]string, requestType config.ChannelType, gpp gpplib.GppContainer) (privacy.PolicyEnforcer, error) { +func extractCCPA(orig *openrtb2.BidRequest, privacyConfig config.Privacy, account *config.Account, requestAliases map[string]string, requestType config.ChannelType, gpp gpplib.GppContainer) (privacy.PolicyEnforcer, error) { // Quick extra wrapper until RequestWrapper makes its way into CleanRequests ccpaPolicy, err := ccpa.ReadFromRequestWrapper(&openrtb_ext.RequestWrapper{BidRequest: orig}, gpp) if err != nil { return privacy.NilPolicyEnforcer{}, err } - validBidders := GetValidBidders(aliases) + validBidders := GetValidBidders(requestAliases) ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders) if err != nil { return privacy.NilPolicyEnforcer{}, err @@ -312,147 +490,47 @@ func ExtractReqExtBidderParamsMap(bidRequest *openrtb2.BidRequest) (map[string]j return bidderParams, nil } -func getAuctionBidderRequests(auctionRequest AuctionRequest, - requestExt *openrtb_ext.ExtRequest, - bidderToSyncerKey map[string]string, - impsByBidder map[string][]openrtb2.Imp, - aliases map[string]string, - hostSChainNode *openrtb2.SupplyChainNode) ([]BidderRequest, []error) { - - bidderRequests := make([]BidderRequest, 0, len(impsByBidder)) - req := auctionRequest.BidRequestWrapper - explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User) +func buildRequestExtForBidder(bidder string, req *openrtb_ext.RequestWrapper, reqExtBidderParams map[string]json.RawMessage, cfgABC *openrtb_ext.ExtAlternateBidderCodes) error { + reqExt, err := req.GetRequestExt() if err != nil { - return nil, []error{err} - } - - bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) - if err != nil { - return nil, []error{err} - } - - sChainWriter, err := schain.NewSChainWriter(requestExt, hostSChainNode) - if err != nil { - return nil, []error{err} - } - - lowerCaseExplicitBuyerUIDs := make(map[string]string) - for bidder, uid := range explicitBuyerUIDs { - lowerKey := strings.ToLower(bidder) - lowerCaseExplicitBuyerUIDs[lowerKey] = uid - } - - var errs []error - for bidder, imps := range impsByBidder { - coreBidder, isRequestAlias := resolveBidder(bidder, aliases) - - reqCopy := *req.BidRequest - reqCopy.Imp = imps - - sChainWriter.Write(&reqCopy, bidder) - - reqCopy.Ext, err = buildRequestExtForBidder(bidder, req.BidRequest.Ext, requestExt, bidderParamsInReqExt, auctionRequest.Account.AlternateBidderCodes) - if err != nil { - return nil, []error{err} - } - - if err := removeUnpermissionedEids(&reqCopy, bidder, requestExt); err != nil { - errs = append(errs, fmt.Errorf("unable to enforce request.ext.prebid.data.eidpermissions because %v", err)) - continue - } - - bidderRequest := BidderRequest{ - BidderName: openrtb_ext.BidderName(bidder), - BidderCoreName: coreBidder, - IsRequestAlias: isRequestAlias, - BidRequest: &reqCopy, - BidderLabels: metrics.AdapterLabels{ - Source: auctionRequest.LegacyLabels.Source, - RType: auctionRequest.LegacyLabels.RType, - Adapter: coreBidder, - PubID: auctionRequest.LegacyLabels.PubID, - CookieFlag: auctionRequest.LegacyLabels.CookieFlag, - AdapterBids: metrics.AdapterBidPresent, - }, - } - - syncerKey := bidderToSyncerKey[string(coreBidder)] - if hadSync := prepareUser(&reqCopy, bidder, syncerKey, lowerCaseExplicitBuyerUIDs, auctionRequest.UserSyncs); !hadSync && req.BidRequest.App == nil { - bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagNo - } else { - bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagYes - } - - bidderRequests = append(bidderRequests, bidderRequest) + return err } - return bidderRequests, errs -} + prebid := reqExt.GetPrebid() -func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, requestExtParsed *openrtb_ext.ExtRequest, bidderParamsInReqExt map[string]json.RawMessage, cfgABC *openrtb_ext.ExtAlternateBidderCodes) (json.RawMessage, error) { - // Resolve alternatebiddercode for current bidder + // Resolve alternatebiddercode var reqABC *openrtb_ext.ExtAlternateBidderCodes - if len(requestExt) != 0 && requestExtParsed != nil && requestExtParsed.Prebid.AlternateBidderCodes != nil { - reqABC = requestExtParsed.Prebid.AlternateBidderCodes + if prebid != nil && prebid.AlternateBidderCodes != nil { + reqABC = prebid.AlternateBidderCodes } alternateBidderCodes := buildRequestExtAlternateBidderCodes(bidder, cfgABC, reqABC) - if (len(requestExt) == 0 || requestExtParsed == nil) && alternateBidderCodes == nil { - return nil, nil - } - - // Resolve Bidder Params - var bidderParams json.RawMessage - if bidderParamsInReqExt != nil { - bidderParams = bidderParamsInReqExt[bidder] - } - - // Copy Allowed Fields - // Per: https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#prebid-server-ortb2-extension-summary - prebid := openrtb_ext.ExtRequestPrebid{ - BidderParams: bidderParams, - AlternateBidderCodes: alternateBidderCodes, - } - - if requestExtParsed != nil { - prebid.Channel = requestExtParsed.Prebid.Channel - prebid.CurrencyConversions = requestExtParsed.Prebid.CurrencyConversions - prebid.Debug = requestExtParsed.Prebid.Debug - prebid.Integration = requestExtParsed.Prebid.Integration - prebid.MultiBid = buildRequestExtMultiBid(bidder, requestExtParsed.Prebid.MultiBid, alternateBidderCodes) - prebid.Sdk = requestExtParsed.Prebid.Sdk - prebid.Server = requestExtParsed.Prebid.Server - } - - // Marshal New Prebid Object - prebidJson, err := jsonutil.Marshal(prebid) - if err != nil { - return nil, err - } - - // Parse Existing Ext - extMap := make(map[string]json.RawMessage) - if len(requestExt) != 0 { - if err := jsonutil.Unmarshal(requestExt, &extMap); err != nil { - return nil, err + var prebidNew openrtb_ext.ExtRequestPrebid + if prebid == nil { + prebidNew = openrtb_ext.ExtRequestPrebid{ + BidderParams: reqExtBidderParams[bidder], + AlternateBidderCodes: alternateBidderCodes, } - } - - // Update Ext With Prebid Json - if bytes.Equal(prebidJson, []byte(`{}`)) { - delete(extMap, "prebid") } else { - extMap["prebid"] = prebidJson + // Copy Allowed Fields + // Per: https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#prebid-server-ortb2-extension-summary + prebidNew = openrtb_ext.ExtRequestPrebid{ + BidderParams: reqExtBidderParams[bidder], + AlternateBidderCodes: alternateBidderCodes, + Channel: prebid.Channel, + CurrencyConversions: prebid.CurrencyConversions, + Debug: prebid.Debug, + Integration: prebid.Integration, + MultiBid: buildRequestExtMultiBid(bidder, prebid.MultiBid, alternateBidderCodes), + Sdk: prebid.Sdk, + Server: prebid.Server, + } } - if len(extMap) > 0 { - return jsonutil.Marshal(extMap) - } else { - return nil, nil - } + reqExt.SetPrebid(&prebidNew) + return nil } func buildRequestExtAlternateBidderCodes(bidder string, accABC *openrtb_ext.ExtAlternateBidderCodes, reqABC *openrtb_ext.ExtAlternateBidderCodes) *openrtb_ext.ExtAlternateBidderCodes { - if altBidderCodes := copyExtAlternateBidderCodes(bidder, reqABC); altBidderCodes != nil { return altBidderCodes } @@ -564,7 +642,7 @@ func extractBuyerUIDs(user *openrtb2.User) (map[string]string, error) { // The "imp.ext" value of the rubicon Imp will only contain the "prebid" values, and "rubicon" value at the "bidder" key. // // The goal here is so that Bidders only get Imps and Imp.Ext values which are intended for them. -func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) { +func splitImps(imps []openrtb2.Imp, requestValidator ortb.RequestValidator, requestAliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) (map[string][]openrtb2.Imp, error) { bidderImps := make(map[string][]openrtb2.Imp) for i, imp := range imps { @@ -585,6 +663,11 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) { jsonutil.Unmarshal(impExtPrebidBidderJSON, &impExtPrebidBidder) } + var impExtPrebidImp map[string]json.RawMessage + if impExtPrebidImpJSON, exists := impExtPrebid["imp"]; exists { + jsonutil.Unmarshal(impExtPrebidImpJSON, &impExtPrebidImp) + } + sanitizedImpExt, err := createSanitizedImpExt(impExt, impExtPrebid) if err != nil { return nil, fmt.Errorf("unable to remove other bidder fields for imp[%d]: %v", i, err) @@ -593,6 +676,22 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) { for bidder, bidderExt := range impExtPrebidBidder { impCopy := imp + if impBidderFPD, exists := impExtPrebidImp[bidder]; exists { + if err := mergeImpFPD(&impCopy, impBidderFPD, i); err != nil { + return nil, err + } + impWrapper := openrtb_ext.ImpWrapper{Imp: &impCopy} + cfg := ortb.ValidationConfig{ + SkipBidderParams: true, + SkipNative: true, + } + if err := requestValidator.ValidateImp(&impWrapper, cfg, i, requestAliases, hasStoredAuctionResponses, storedBidResponses); err != nil { + return nil, &errortypes.InvalidImpFirstPartyData{ + Message: fmt.Sprintf("merging bidder imp first party data for imp %s results in an invalid imp: %v", imp.ID, err), + } + } + } + sanitizedImpExt[openrtb_ext.PrebidExtBidderKey] = bidderExt impExtJSON, err := jsonutil.Marshal(sanitizedImpExt) @@ -608,6 +707,16 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) { return bidderImps, nil } +func mergeImpFPD(imp *openrtb2.Imp, fpd json.RawMessage, index int) error { + if err := jsonutil.MergeClone(imp, fpd); err != nil { + if strings.Contains(err.Error(), "invalid json on existing object") { + return fmt.Errorf("invalid imp ext for imp[%d]", index) + } + return fmt.Errorf("invalid first party data for imp[%d]", index) + } + return nil +} + var allowedImpExtFields = map[string]interface{}{ openrtb_ext.AuctionEnvironmentKey: struct{}{}, openrtb_ext.FirstPartyDataExtKey: struct{}{}, @@ -657,7 +766,7 @@ func createSanitizedImpExt(impExt, impExtPrebid map[string]json.RawMessage) (map // // In this function, "givenBidder" may or may not be an alias. "coreBidder" must *not* be an alias. // It returns true if a Cookie User Sync existed, and false otherwise. -func prepareUser(req *openrtb2.BidRequest, givenBidder, syncerKey string, explicitBuyerUIDs map[string]string, usersyncs IdFetcher) bool { +func prepareUser(req *openrtb_ext.RequestWrapper, givenBidder, syncerKey string, explicitBuyerUIDs map[string]string, usersyncs IdFetcher) bool { cookieId, hadCookie, _ := usersyncs.GetUID(syncerKey) if id, ok := explicitBuyerUIDs[strings.ToLower(givenBidder)]; ok { @@ -685,42 +794,32 @@ func copyWithBuyerUID(user *openrtb2.User, buyerUID string) *openrtb2.User { return user } -// removeUnpermissionedEids modifies the request to remove any request.user.ext.eids not permissions for the specific bidder -func removeUnpermissionedEids(request *openrtb2.BidRequest, bidder string, requestExt *openrtb_ext.ExtRequest) error { +// removeUnpermissionedEids modifies the request to remove any request.user.eids not permissions for the specific bidder +func removeUnpermissionedEids(reqWrapper *openrtb_ext.RequestWrapper, bidder string) error { // ensure request might have eids (as much as we can check before unmarshalling) - if request.User == nil || len(request.User.Ext) == 0 { + if reqWrapper.User == nil || len(reqWrapper.User.EIDs) == 0 { return nil } // ensure request has eid permissions to enforce - if requestExt == nil || requestExt.Prebid.Data == nil || len(requestExt.Prebid.Data.EidPermissions) == 0 { - return nil - } - - // low level unmarshal to preserve other request.user.ext values. prebid server is non-destructive. - var userExt map[string]json.RawMessage - if err := jsonutil.Unmarshal(request.User.Ext, &userExt); err != nil { + reqExt, err := reqWrapper.GetRequestExt() + if err != nil { return err } - - eidsJSON, eidsSpecified := userExt["eids"] - if !eidsSpecified { + if reqExt == nil { return nil } - var eids []openrtb2.EID - if err := jsonutil.Unmarshal(eidsJSON, &eids); err != nil { - return err - } - - // exit early if there are no eids (empty array) - if len(eids) == 0 { + reqExtPrebid := reqExt.GetPrebid() + if reqExtPrebid == nil || reqExtPrebid.Data == nil || len(reqExtPrebid.Data.EidPermissions) == 0 { return nil } + eids := reqWrapper.User.EIDs + // translate eid permissions to a map for quick lookup eidRules := make(map[string][]string) - for _, p := range requestExt.Prebid.Data.EidPermissions { + for _, p := range reqExtPrebid.Data.EidPermissions { eidRules[p.Source] = p.Bidders } @@ -748,37 +847,14 @@ func removeUnpermissionedEids(request *openrtb2.BidRequest, bidder string, reque return nil } - // marshal eidsAllowed back to userExt if len(eidsAllowed) == 0 { - delete(userExt, "eids") + reqWrapper.User.EIDs = nil } else { - eidsRaw, err := jsonutil.Marshal(eidsAllowed) - if err != nil { - return err - } - userExt["eids"] = eidsRaw - } - - // exit early if userExt is empty - if len(userExt) == 0 { - setUserExtWithCopy(request, nil) - return nil + reqWrapper.User.EIDs = eidsAllowed } - - userExtJSON, err := jsonutil.Marshal(userExt) - if err != nil { - return err - } - setUserExtWithCopy(request, userExtJSON) return nil } -func setUserExtWithCopy(request *openrtb2.BidRequest, userExtJSON json.RawMessage) { - userCopy := *request.User - userCopy.Ext = userExtJSON - request.User = &userCopy -} - // resolveBidder returns the known BidderName associated with bidder, if bidder is an alias. If it's not an alias, the bidder is returned. func resolveBidder(bidder string, requestAliases map[string]string) (openrtb_ext.BidderName, bool) { normalisedBidderName, _ := openrtb_ext.NormalizeBidderName(bidder) @@ -790,36 +866,23 @@ func resolveBidder(bidder string, requestAliases map[string]string) (openrtb_ext return normalisedBidderName, false } -// parseAliases parses the aliases from the BidRequest -func parseAliases(orig *openrtb2.BidRequest) (map[string]string, []error) { - var aliases map[string]string - if value, dataType, _, err := jsonparser.Get(orig.Ext, openrtb_ext.PrebidExtKey, "aliases"); dataType == jsonparser.Object && err == nil { - if err := jsonutil.Unmarshal(value, &aliases); err != nil { - return nil, []error{err} - } - } else if dataType != jsonparser.NotExist && err != jsonparser.KeyPathNotFoundError { - return nil, []error{err} +func getRequestAliases(req *openrtb_ext.RequestWrapper) (map[string]string, map[string]uint16, []error) { + reqExt, err := req.GetRequestExt() + if err != nil { + return nil, nil, []error{errInvalidRequestExt} } - return aliases, nil -} -// parseAliasesGVLIDs parses the Bidder Alias GVLIDs from the BidRequest -func parseAliasesGVLIDs(orig *openrtb2.BidRequest) (map[string]uint16, []error) { - var aliasesGVLIDs map[string]uint16 - if value, dataType, _, err := jsonparser.Get(orig.Ext, openrtb_ext.PrebidExtKey, "aliasgvlids"); dataType == jsonparser.Object && err == nil { - if err := jsonutil.Unmarshal(value, &aliasesGVLIDs); err != nil { - return nil, []error{err} - } - } else if dataType != jsonparser.NotExist && err != jsonparser.KeyPathNotFoundError { - return nil, []error{err} + if prebid := reqExt.GetPrebid(); prebid != nil { + return prebid.Aliases, prebid.AliasGVLIDs, nil } - return aliasesGVLIDs, nil + + return nil, nil, nil } -func GetValidBidders(aliases map[string]string) map[string]struct{} { +func GetValidBidders(requestAliases map[string]string) map[string]struct{} { validBidders := openrtb_ext.BuildBidderNameHashSet() - for k := range aliases { + for k := range requestAliases { validBidders[k] = struct{}{} } @@ -924,14 +987,19 @@ func getExtBidAdjustmentFactors(requestExtPrebid *openrtb_ext.ExtRequestPrebid) return nil } -func applyFPD(fpd map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData, r BidderRequest) { +func applyFPD(fpd map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData, + coreBidderName openrtb_ext.BidderName, + bidderName openrtb_ext.BidderName, + isRequestAlias bool, + reqWrapper *openrtb_ext.RequestWrapper, + fpdUserEIDsPresent bool) { if fpd == nil { return } - bidder := r.BidderCoreName - if r.IsRequestAlias { - bidder = r.BidderName + bidder := coreBidderName + if isRequestAlias { + bidder = bidderName } fpdToApply, exists := fpd[bidder] @@ -940,77 +1008,31 @@ func applyFPD(fpd map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyD } if fpdToApply.Site != nil { - r.BidRequest.Site = fpdToApply.Site + reqWrapper.Site = fpdToApply.Site } if fpdToApply.App != nil { - r.BidRequest.App = fpdToApply.App + reqWrapper.App = fpdToApply.App } if fpdToApply.User != nil { - //BuyerUID is a value obtained between fpd extraction and fpd application. - //BuyerUID needs to be set back to fpd before applying this fpd to final bidder request - if r.BidRequest.User != nil && len(r.BidRequest.User.BuyerUID) > 0 { - fpdToApply.User.BuyerUID = r.BidRequest.User.BuyerUID - } - r.BidRequest.User = fpdToApply.User - } -} - -func buildBidResponseRequest(req *openrtb2.BidRequest, - bidderImpResponses stored_responses.BidderImpsWithBidResponses, - aliases map[string]string, - bidderImpReplaceImpID stored_responses.BidderImpReplaceImpID) map[openrtb_ext.BidderName]BidderRequest { - - bidderToBidderResponse := make(map[openrtb_ext.BidderName]BidderRequest) - - for bidderName, impResps := range bidderImpResponses { - resolvedBidder, isRequestAlias := resolveBidder(string(bidderName), aliases) - bidderToBidderResponse[bidderName] = BidderRequest{ - BidRequest: req, - BidderCoreName: resolvedBidder, - BidderName: bidderName, - BidderStoredResponses: impResps, - ImpReplaceImpId: bidderImpReplaceImpID[string(bidderName)], - IsRequestAlias: isRequestAlias, - BidderLabels: metrics.AdapterLabels{Adapter: resolvedBidder}, - } - } - return bidderToBidderResponse -} - -func mergeBidderRequests(allBidderRequests []BidderRequest, bidderNameToBidderReq map[openrtb_ext.BidderName]BidderRequest) []BidderRequest { - if len(allBidderRequests) == 0 && len(bidderNameToBidderReq) == 0 { - return allBidderRequests - } - if len(allBidderRequests) == 0 && len(bidderNameToBidderReq) > 0 { - for _, v := range bidderNameToBidderReq { - allBidderRequests = append(allBidderRequests, v) - } - return allBidderRequests - } else if len(allBidderRequests) > 0 && len(bidderNameToBidderReq) > 0 { - //merge bidder requests with real imps and imps with stored resp - for bn, br := range bidderNameToBidderReq { - found := false - for i, ar := range allBidderRequests { - if ar.BidderName == bn { - //bidder req with real imps and imps with stored resp - allBidderRequests[i].BidderStoredResponses = br.BidderStoredResponses - found = true - break - } + if reqWrapper.User != nil { + if len(reqWrapper.User.BuyerUID) > 0 { + //BuyerUID is a value obtained between fpd extraction and fpd application. + //BuyerUID needs to be set back to fpd before applying this fpd to final bidder request + fpdToApply.User.BuyerUID = reqWrapper.User.BuyerUID } - if !found { - //bidder req with stored bid responses only - br.BidRequest.Imp = nil // to indicate this bidder request has bidder responses only - allBidderRequests = append(allBidderRequests, br) + + // if FPD config didn't have user.eids - use reqWrapper.User.EIDs after removeUnpermissionedEids + if !fpdUserEIDsPresent { + fpdToApply.User.EIDs = reqWrapper.User.EIDs } } + reqWrapper.User = fpdToApply.User } - return allBidderRequests } -func setLegacyGDPRFromGPP(r *openrtb2.BidRequest, gpp gpplib.GppContainer) { +func setLegacyGDPRFromGPP(r *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) { if r.Regs != nil && r.Regs.GDPR == nil { if r.Regs.GPPSID != nil { // Set to 0 unless SID exists @@ -1039,13 +1061,12 @@ func setLegacyGDPRFromGPP(r *openrtb2.BidRequest, gpp gpplib.GppContainer) { } } } - } -func setLegacyUSPFromGPP(r *openrtb2.BidRequest, gpp gpplib.GppContainer) { + +func setLegacyUSPFromGPP(r *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) { if r.Regs == nil { return } - if len(r.Regs.USPrivacy) > 0 || r.Regs.GPPSID == nil { return } @@ -1060,7 +1081,6 @@ func setLegacyUSPFromGPP(r *openrtb2.BidRequest, gpp gpplib.GppContainer) { } } } - } func WrapJSONInData(data []byte) []byte { @@ -1121,24 +1141,20 @@ func getPrebidMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { } } -func applyBidAdjustmentToFloor(allBidderRequests []BidderRequest, bidAdjustmentFactors map[string]float64) { - - if len(bidAdjustmentFactors) == 0 { +func applyBidAdjustmentToFloor(req *openrtb_ext.RequestWrapper, bidder string, adjustmentFactors map[string]float64) { + if len(adjustmentFactors) == 0 { return } - for _, bidderRequest := range allBidderRequests { - bidAdjustment := 1.0 - - if bidAdjustemntValue, ok := bidAdjustmentFactors[string(bidderRequest.BidderName)]; ok { - bidAdjustment = bidAdjustemntValue - } + bidAdjustment := 1.0 + if v, ok := adjustmentFactors[bidder]; ok && v != 0.0 { + bidAdjustment = v + } - if bidAdjustment != 1.0 { - for index, imp := range bidderRequest.BidRequest.Imp { - imp.BidFloor = imp.BidFloor / bidAdjustment - bidderRequest.BidRequest.Imp[index] = imp - } + if bidAdjustment != 1.0 { + for index, imp := range req.Imp { + imp.BidFloor = imp.BidFloor / bidAdjustment + req.Imp[index] = imp } } } diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 0c8153b6b11..0d6f094aece 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -4,12 +4,9 @@ import ( "context" "encoding/json" "errors" - "fmt" "sort" "testing" - "github.com/prebid/prebid-server/v2/stored_responses" - gpplib "github.com/prebid/go-gpp" "github.com/prebid/go-gpp/constants" "github.com/prebid/openrtb/v20/openrtb2" @@ -24,6 +21,7 @@ import ( "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) const deviceUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" @@ -45,7 +43,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (gdpr.AuctionPermissions, error) { +func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions { permissions := gdpr.AuctionPermissions{ PassGeo: p.passGeo, PassID: p.passID, @@ -53,7 +51,7 @@ func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCo if p.allowAllBidders { permissions.AllowBidRequest = true - return permissions, p.activitiesError + return permissions } for _, allowedBidder := range p.allowedBidders { @@ -62,7 +60,7 @@ func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCo } } - return permissions, p.activitiesError + return permissions } type fakePermissionsBuilder struct { @@ -92,10 +90,11 @@ func assertReq(t *testing.T, bidderRequests []BidderRequest, func TestSplitImps(t *testing.T) { testCases := []struct { - description string - givenImps []openrtb2.Imp - expectedImps map[string][]openrtb2.Imp - expectedError string + description string + givenImps []openrtb2.Imp + validatorErrors []error + expectedImps map[string][]openrtb2.Imp + expectedError string }{ { description: "Nil", @@ -208,10 +207,105 @@ func TestSplitImps(t *testing.T) { }, expectedError: "invalid json for imp[0]: do not know how to skip: 109", }, + { + description: "Malformed imp.ext.prebid.imp", + givenImps: []openrtb2.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"imp": malformed}}`)}, + }, + expectedError: "invalid json for imp[0]: do not know how to skip: 109", + }, + { + description: "valid FPD at imp.ext.prebid.imp for valid bidder", + givenImps: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + }, + }, + }, + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":30,"h":40}]}}}}}`), + }, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderA": { + { + ID: "impFPD", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 30, + H: 40, + }, + }, + }, + Ext: json.RawMessage(`{"bidder":{"imp1paramA":"imp1valueA"}}`), + }, + }, + }, + expectedError: "", + }, + { + description: "valid FPD at imp.ext.prebid.imp for unknown bidder", + givenImps: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + }, + }, + }, + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderB":{"imp1paramB":"imp1valueB"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":30,"h":40}]}}}}}`), + }, + }, + expectedImps: map[string][]openrtb2.Imp{ + "bidderB": { + { + ID: "imp1", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + }, + }, + }, + Ext: json.RawMessage(`{"bidder":{"imp1paramB":"imp1valueB"}}`), + }, + }, + }, + expectedError: "", + }, + { + description: "invalid FPD at imp.ext.prebid.imp for valid bidder", + givenImps: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + }, + }, + }, + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":0,"h":0}]}}}}}`), + }, + }, + validatorErrors: []error{errors.New("some error")}, + expectedImps: nil, + expectedError: "merging bidder imp first party data for imp imp1 results in an invalid imp: [some error]", + }, } for _, test := range testCases { - imps, err := splitImps(test.givenImps) + imps, err := splitImps(test.givenImps, &mockRequestValidator{errors: test.validatorErrors}, nil, false, nil) if test.expectedError == "" { assert.NoError(t, err, test.description+":err") @@ -223,6 +317,201 @@ func TestSplitImps(t *testing.T) { } } +func TestMergeImpFPD(t *testing.T) { + imp1 := &openrtb2.Imp{ + ID: "imp1", + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](400), + }, + } + + tests := []struct { + description string + imp *openrtb2.Imp + fpd json.RawMessage + wantImp *openrtb2.Imp + wantError bool + }{ + { + description: "nil", + imp: nil, + fpd: nil, + wantImp: nil, + wantError: true, + }, + { + description: "nil_fpd", + imp: imp1, + fpd: nil, + wantImp: imp1, + wantError: true, + }, + { + description: "empty_fpd", + imp: imp1, + fpd: json.RawMessage(`{}`), + wantImp: imp1, + wantError: false, + }, + { + description: "nil_imp", + imp: nil, + fpd: json.RawMessage(`{}`), + wantImp: nil, + wantError: true, + }, + { + description: "zero_value_imp", + imp: &openrtb2.Imp{}, + fpd: json.RawMessage(`{}`), + wantImp: &openrtb2.Imp{}, + wantError: false, + }, + { + description: "invalid_json_on_existing_imp", + imp: &openrtb2.Imp{ + Ext: json.RawMessage(`malformed`), + }, + fpd: json.RawMessage(`{"ext": {"a":1}}`), + wantImp: &openrtb2.Imp{ + Ext: json.RawMessage(`malformed`), + }, + wantError: true, + }, + { + description: "invalid_json_in_fpd", + imp: &openrtb2.Imp{ + Ext: json.RawMessage(`{"ext": {"a":1}}`), + }, + fpd: json.RawMessage(`malformed`), + wantImp: &openrtb2.Imp{ + Ext: json.RawMessage(`{"ext": {"a":1}}`), + }, + wantError: true, + }, + { + description: "override_everything", + imp: &openrtb2.Imp{ + ID: "id1", + Metric: []openrtb2.Metric{{Type: "type1", Value: 1, Vendor: "vendor1"}}, + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](1), + H: ptrutil.ToPtr[int64](2), + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + Ext: json.RawMessage(`{"formatkey1":"formatval1"}`), + }, + }, + }, + Instl: 1, + BidFloor: 1, + Ext: json.RawMessage(`{"cool":"test"}`), + }, + fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "h": 200, "format": [{"w":1000, "h":2000, "ext":{"formatkey1":"formatval2"}}]}, "instl":2, "bidfloor":2, "ext":{"cool":"test2"} }`), + wantImp: &openrtb2.Imp{ + ID: "id2", + Metric: []openrtb2.Metric{{Type: "type2", Value: 2, Vendor: "vendor2"}}, + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](100), + H: ptrutil.ToPtr[int64](200), + Format: []openrtb2.Format{ + { + W: 1000, + H: 2000, + Ext: json.RawMessage(`{"formatkey1":"formatval2"}`), + }, + }, + }, + Instl: 2, + BidFloor: 2, + Ext: json.RawMessage(`{"cool":"test2"}`), + }, + }, + { + description: "override_partial_simple", + imp: imp1, + fpd: json.RawMessage(`{"id": "456", "banner": {"format": [{"w":1, "h":2}]} }`), + wantImp: &openrtb2.Imp{ + ID: "456", + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](400), + Format: []openrtb2.Format{ + { + W: 1, + H: 2, + }, + }, + }, + }, + }, + { + description: "override_partial_complex", + imp: &openrtb2.Imp{ + ID: "id1", + Metric: []openrtb2.Metric{{Type: "type1", Value: 1, Vendor: "vendor1"}}, + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](1), + H: ptrutil.ToPtr[int64](2), + Format: []openrtb2.Format{ + { + W: 10, + H: 20, + Ext: json.RawMessage(`{"formatkey1":"formatval1"}`), + }, + }, + }, + Instl: 1, + TagID: "tag1", + BidFloor: 1, + Rwdd: 1, + DT: 1, + IframeBuster: []string{"buster1", "buster2"}, + Ext: json.RawMessage(`{"cool1":"test1", "cool2":"test2"}`), + }, + fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "format": [{"w":1000, "h":2000, "ext":{"formatkey1":"formatval11"}}]}, "instl":2, "bidfloor":2, "ext":{"cool1":"test11"} }`), + wantImp: &openrtb2.Imp{ + ID: "id2", + Metric: []openrtb2.Metric{{Type: "type2", Value: 2, Vendor: "vendor2"}}, + Banner: &openrtb2.Banner{ + W: ptrutil.ToPtr[int64](100), + H: ptrutil.ToPtr[int64](2), + Format: []openrtb2.Format{ + { + W: 1000, + H: 2000, + Ext: json.RawMessage(`{"formatkey1":"formatval11"}`), + }, + }, + }, + Instl: 2, + TagID: "tag1", + BidFloor: 2, + Rwdd: 1, + DT: 1, + IframeBuster: []string{"buster1", "buster2"}, + Ext: json.RawMessage(`{"cool1":"test11","cool2":"test2"}`), + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + err := mergeImpFPD(test.imp, test.fpd, 1) + assert.Equal(t, test.wantImp, test.imp) + + if test.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestCreateSanitizedImpExt(t *testing.T) { testCases := []struct { description string @@ -475,7 +764,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, false, map[string]float64{}) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -541,7 +830,7 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, false, map[string]float64{}) assert.Empty(t, err, "No errors should be returned") for _, bidderRequest := range bidderRequests { bidderName := bidderRequest.BidderName @@ -675,7 +964,7 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { W: ptrutil.ToPtr[int64](300), H: ptrutil.ToPtr[int64](250), }, - Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"}}}}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"},"bidderB":{"placementId":"456"}}}}`), }, }, expectedBidderRequests: map[string]BidderRequest{ @@ -706,7 +995,7 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { W: ptrutil.ToPtr[int64](300), H: ptrutil.ToPtr[int64](250), }, - Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"}}}}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"},"bidderB":{"placementId":"456"}}}}`), }, { ID: "imp-id2", @@ -746,7 +1035,7 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { W: ptrutil.ToPtr[int64](300), H: ptrutil.ToPtr[int64](250), }, - Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"}}}}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"},"bidderB":{"placementId":"456"}}}}`), }, { ID: "imp-id2", @@ -812,11 +1101,11 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { imps: []openrtb2.Imp{ { ID: "imp-id1", - Ext: json.RawMessage(`"prebid": {}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"}}}}`), }, { ID: "imp-id2", - Ext: json.RawMessage(`"prebid": {}`), + Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"placementId":"123"}}}}`), }, }, expectedBidderRequests: map[string]BidderRequest{ @@ -856,7 +1145,7 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { bidderInfo: config.BidderInfos{}, } - actualBidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + actualBidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, map[string]float64{}) assert.Empty(t, err, "No errors should be returned") assert.Len(t, actualBidderRequests, len(test.expectedBidderRequests), "result len doesn't match for testCase %s", test.description) for _, actualBidderRequest := range actualBidderRequests { @@ -987,7 +1276,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { req := newBidRequest(t) req.Ext = test.reqExt req.Regs = &openrtb2.Regs{ - Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`), + USPrivacy: test.ccpaConsent, } privacyConfig := config.Privacy{ @@ -1015,26 +1304,31 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { }, }.Builder + metricsMock := metrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordAdapterBuyerUIDScrubbed", mock.Anything).Return() + bidderToSyncerKey := map[string]string{} reqSplitter := &requestSplitter{ bidderToSyncerKey: bidderToSyncerKey, - me: &metrics.MetricsEngineMock{}, + me: &metricsMock, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, map[string]float64{}) result := bidderRequests[0] assert.Nil(t, errs) if test.expectDataScrub { assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID") assert.Equal(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5") + metricsMock.AssertCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) } else { assert.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5") + metricsMock.AssertNotCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) } assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") } @@ -1042,32 +1336,32 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { testCases := []struct { - description string - reqExt json.RawMessage - reqRegsExt json.RawMessage - expectError error + description string + reqExt json.RawMessage + reqRegsPrivacy string + expectError error }{ { - description: "Invalid Consent", - reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), - reqRegsExt: json.RawMessage(`{"us_privacy":"malformed"}`), + description: "Invalid Consent", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), + reqRegsPrivacy: "malformed", expectError: &errortypes.Warning{ Message: "request.regs.ext.us_privacy must contain 4 characters", WarningCode: errortypes.InvalidPrivacyConsentWarningCode, }, }, { - description: "Invalid No Sale Bidders", - reqExt: json.RawMessage(`{"prebid":{"nosale":["*", "another"]}}`), - reqRegsExt: json.RawMessage(`{"us_privacy":"1NYN"}`), - expectError: errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided"), + description: "Invalid No Sale Bidders", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*", "another"]}}`), + reqRegsPrivacy: "1NYN", + expectError: errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided"), }, } for _, test := range testCases { req := newBidRequest(t) req.Ext = test.reqExt - req.Regs = &openrtb2.Regs{Ext: test.reqRegsExt} + req.Regs = &openrtb2.Regs{USPrivacy: test.reqRegsPrivacy} var reqExtStruct openrtb_ext.ExtRequest err := jsonutil.UnmarshalValid(req.Ext, &reqExtStruct) @@ -1102,7 +1396,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { bidderInfo: config.BidderInfos{}, } - _, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, gdpr.SignalNo, map[string]float64{}) + _, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, gdpr.SignalNo, false, map[string]float64{}) assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) } @@ -1161,7 +1455,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, map[string]float64{}) result := bidderRequests[0] assert.Nil(t, errs) @@ -1183,46 +1477,114 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { testCases := []struct { description string inExt json.RawMessage - inSourceExt json.RawMessage + inSChain *openrtb2.SupplyChain outRequestExt json.RawMessage - outSourceExt json.RawMessage + outSource *openrtb2.Source hasError bool + ortbVersion string }{ { description: "nil", inExt: nil, - inSourceExt: nil, + inSChain: nil, outRequestExt: nil, - outSourceExt: nil, + outSource: &openrtb2.Source{ + TID: "testTID", + SChain: nil, + Ext: nil, + }, }, { - description: "ORTB 2.5 chain at source.ext.schain", - inExt: nil, - inSourceExt: json.RawMessage(`{` + seller1SChain + `}`), + description: "Supply Chain defined in request.Source.supplyChain", + inExt: nil, + inSChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, outRequestExt: nil, - outSourceExt: json.RawMessage(`{` + seller1SChain + `}`), + outSource: &openrtb2.Source{ + TID: "testTID", + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, + ortbVersion: "2.6", }, { - description: "ORTB 2.5 schain at request.ext.prebid.schains", + description: "Supply Chain defined in request.ext.prebid.schains", inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - inSourceExt: nil, + inSChain: nil, outRequestExt: nil, - outSourceExt: json.RawMessage(`{` + seller1SChain + `}`), + outSource: &openrtb2.Source{ + TID: "testTID", + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, + ortbVersion: "2.6", }, { - description: "schainwriter instantation error -- multiple bidder schains in ext.prebid.schains.", - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), - inSourceExt: json.RawMessage(`{` + seller1SChain + `}`), + description: "schainwriter instantation error -- multiple bidder schains in ext.prebid.schains.", + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + inSChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + outRequestExt: nil, - outSourceExt: nil, + outSource: nil, hasError: true, }, } for _, test := range testCases { req := newBidRequest(t) - if test.inSourceExt != nil { - req.Source.Ext = test.inSourceExt + if test.inSChain != nil { + req.Source.SChain = test.inSChain } var extRequest *openrtb_ext.ExtRequest @@ -1251,17 +1613,17 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, hostSChainNode: nil, - bidderInfo: config.BidderInfos{}, + bidderInfo: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{Version: test.ortbVersion}}}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, false, map[string]float64{}) if test.hasError == true { assert.NotNil(t, errs) assert.Len(t, bidderRequests, 0) } else { result := bidderRequests[0] assert.Nil(t, errs) - assert.Equal(t, test.outSourceExt, result.BidRequest.Source.Ext, test.description+":Source.Ext") + assert.Equal(t, test.outSource, result.BidRequest.Source, test.description+":Source") assert.Equal(t, test.outRequestExt, result.BidRequest.Ext, test.description+":Ext") } } @@ -1325,7 +1687,7 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, false, map[string]float64{}) if test.hasError == true { assert.NotNil(t, errs) assert.Len(t, bidderRequests, 0) @@ -1917,7 +2279,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { bidderInfo: config.BidderInfos{}, } - results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, map[string]float64{}) result := results[0] assert.Nil(t, errs) @@ -1934,160 +2296,57 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { func TestCleanOpenRTBRequestsGDPR(t *testing.T) { tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA" - trueValue, falseValue := true, false testCases := []struct { description string - gdprAccountEnabled *bool - gdprHostEnabled bool - gdpr string gdprConsent string gdprScrub bool + gdprSignal gdpr.Signal + gdprEnforced bool permissionsError error - gdprDefaultValue string expectPrivacyLabels metrics.PrivacyLabels expectError bool }{ { - description: "Enforce - TCF Invalid", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: "malformed", - gdprScrub: false, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: "", - }, - }, - { - description: "Enforce", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: true, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV2, - }, - }, - { - description: "Not Enforce", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: true, - gdpr: "0", - gdprConsent: tcf2Consent, - gdprScrub: false, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: false, - GDPRTCFVersion: "", - }, - }, - { - description: "Enforce; GDPR signal extraction error", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: true, - gdpr: "0{", - gdprConsent: tcf2Consent, - gdprScrub: true, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV2, - }, - expectError: true, - }, - { - description: "Enforce; account GDPR enabled, host GDPR setting disregarded", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: false, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: true, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV2, - }, - }, - { - description: "Not Enforce; account GDPR disabled, host GDPR setting disregarded", - gdprAccountEnabled: &falseValue, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: false, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: false, - GDPRTCFVersion: "", - }, - }, - { - description: "Enforce; account GDPR not specified, host GDPR enabled", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: true, - gdprDefaultValue: "1", + description: "enforce no scrub - TCF invalid", + gdprConsent: "malformed", + gdprScrub: false, + gdprSignal: gdpr.SignalYes, + gdprEnforced: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV2, - }, - }, - { - description: "Not Enforce; account GDPR not specified, host GDPR disabled", - gdprAccountEnabled: nil, - gdprHostEnabled: false, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: false, - gdprDefaultValue: "1", - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: false, GDPRTCFVersion: "", }, }, { - description: "Enforce - Ambiguous signal, don't sync user if ambiguous", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "null", - gdprConsent: tcf2Consent, - gdprScrub: true, - gdprDefaultValue: "1", + description: "enforce and scrub", + gdprConsent: tcf2Consent, + gdprScrub: true, + gdprSignal: gdpr.SignalYes, + gdprEnforced: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, }, }, { - description: "Not Enforce - Ambiguous signal, sync user if ambiguous", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "null", - gdprConsent: tcf2Consent, - gdprScrub: false, - gdprDefaultValue: "0", + description: "not enforce", + gdprConsent: tcf2Consent, + gdprScrub: false, + gdprSignal: gdpr.SignalYes, + gdprEnforced: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", }, }, { - description: "Enforce - error while checking if personal info is allowed", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: tcf2Consent, - gdprScrub: true, - permissionsError: errors.New("Some error"), - gdprDefaultValue: "1", + description: "enforce - error while checking if personal info is allowed", + gdprConsent: tcf2Consent, + gdprScrub: true, + permissionsError: errors.New("Some error"), + gdprSignal: gdpr.SignalYes, + gdprEnforced: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -2097,25 +2356,10 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { for _, test := range testCases { req := newBidRequest(t) - req.User.Ext = json.RawMessage(`{"consent":"` + test.gdprConsent + `"}`) - req.Regs = &openrtb2.Regs{ - Ext: json.RawMessage(`{"gdpr":` + test.gdpr + `}`), - } - - privacyConfig := config.Privacy{ - GDPR: config.GDPR{ - DefaultValue: test.gdprDefaultValue, - TCF2: config.TCF2{ - Enabled: test.gdprHostEnabled, - }, - }, - } + req.User.Consent = test.gdprConsent - accountConfig := config.Account{ - GDPR: config.AccountGDPR{ - Enabled: test.gdprAccountEnabled, - }, - } + privacyConfig := config.Privacy{} + accountConfig := config.Account{} auctionReq := AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, @@ -2136,21 +2380,19 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { }, }.Builder - gdprDefaultValue := gdpr.SignalYes - if test.gdprDefaultValue == "0" { - gdprDefaultValue = gdpr.SignalNo - } + metricsMock := metrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordAdapterBuyerUIDScrubbed", mock.Anything).Return() reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, - me: &metrics.MetricsEngineMock{}, + me: &metricsMock, privacyConfig: privacyConfig, gdprPermsBuilder: gdprPermissionsBuilder, hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdprDefaultValue, map[string]float64{}) + results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, test.gdprSignal, test.gdprEnforced, map[string]float64{}) result := results[0] if test.expectError { @@ -2162,9 +2404,11 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { if test.gdprScrub { assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID") assert.Equal(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5") + metricsMock.AssertCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) } else { assert.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5") + metricsMock.AssertNotCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) } assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") } @@ -2208,15 +2452,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { } req.Imp[0].Ext = json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}, "rubicon": {}}}}`) - privacyConfig := config.Privacy{ - GDPR: config.GDPR{ - DefaultValue: "0", - TCF2: config.TCF2{ - Enabled: test.gdprEnforced, - }, - }, - } - + privacyConfig := config.Privacy{} accountConfig := config.Account{ GDPR: config.AccountGDPR{ Enabled: nil, @@ -2251,7 +2487,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { bidderInfo: config.BidderInfos{}, } - results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalYes, test.gdprEnforced, map[string]float64{}) // extract bidder name from each request in the results bidders := []openrtb_ext.BidderName{} @@ -2302,14 +2538,14 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, expectRegs: &downgradedRegs, expectUser: &downgradedUser, - bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: false}}}, + bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: false, Version: "2.6"}}}, }, { name: "Supported", req: AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config}, expectRegs: bidReq.Regs, expectUser: bidReq.User, - bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: true}}}, + bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: true, Version: "2.6"}}}, }, } @@ -2339,7 +2575,7 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { hostSChainNode: nil, bidderInfo: test.bidderInfos, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, false, map[string]float64{}) assert.Nil(t, err, "Err should be nil") bidRequest := bidderRequests[0] assert.Equal(t, test.expectRegs, bidRequest.BidRequest.Regs) @@ -2356,145 +2592,146 @@ func TestBuildRequestExtForBidder(t *testing.T) { ) testCases := []struct { - description string + name string requestExt json.RawMessage bidderParams map[string]json.RawMessage alternateBidderCodes *openrtb_ext.ExtAlternateBidderCodes expectedJson json.RawMessage }{ { - description: "Nil", + name: "Nil", bidderParams: nil, requestExt: nil, alternateBidderCodes: nil, expectedJson: nil, }, { - description: "Empty", + name: "Empty", bidderParams: nil, alternateBidderCodes: nil, requestExt: json.RawMessage(`{}`), expectedJson: nil, }, { - description: "Prebid - Allowed Fields Only", + name: "Prebid - Allowed Fields Only", bidderParams: nil, requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), }, { - description: "Prebid - Allowed Fields + Bidder Params", + name: "Prebid - Allowed Fields + Bidder Params", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`), }, { - description: "Other", + name: "Other", bidderParams: nil, requestExt: json.RawMessage(`{"other":"foo"}`), expectedJson: json.RawMessage(`{"other":"foo"}`), }, { - description: "Prebid + Other + Bider Params", + name: "Prebid + Other + Bider Params", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`), }, { - description: "Prebid + AlternateBidderCodes in pbs config but current bidder not in AlternateBidderCodes config", + name: "Prebid + AlternateBidderCodes in pbs config but current bidder not in AlternateBidderCodes config", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, alternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{"bar": {Enabled: true, AllowedBidderCodes: []string{"*"}}}}, requestExt: json.RawMessage(`{"other":"foo"}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"alternatebiddercodes":{"enabled":true,"bidders":null},"bidderparams":"bar"}}`), }, { - description: "Prebid + AlternateBidderCodes in request", + name: "Prebid + AlternateBidderCodes in request", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, alternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]},"bar":{"enabled":true,"allowedbiddercodes":["ix"]}}}}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}},"bidderparams":"bar"}}`), }, { - description: "Prebid + AlternateBidderCodes in request but current bidder not in AlternateBidderCodes config", + name: "Prebid + AlternateBidderCodes in request but current bidder not in AlternateBidderCodes config", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, alternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"bar":{"enabled":true,"allowedbiddercodes":["ix"]}}}}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":null},"bidderparams":"bar"}}`), }, { - description: "Prebid + AlternateBidderCodes in both pbs config and in the request", + name: "Prebid + AlternateBidderCodes in both pbs config and in the request", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, alternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{Enabled: true, Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{"foo": {Enabled: true, AllowedBidderCodes: []string{"*"}}}}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]},"bar":{"enabled":true,"allowedbiddercodes":["ix"]}}}}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}},"bidderparams":"bar"}}`), }, { - description: "Prebid + Other + Bider Params + MultiBid.Bidder", + name: "Prebid + Other + Bider Params + MultiBid.Bidder", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo","maxbids":2,"targetbiddercodeprefix":"fmb"}],"bidderparams":"bar"}}`), }, { - description: "Prebid + Other + Bider Params + MultiBid.Bidders", + name: "Prebid + Other + Bider Params + MultiBid.Bidders", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"pubmatic","maxbids":3,"targetbiddercodeprefix":"pubM"},{"bidders":["foo","groupm"],"maxbids":4}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidders":["foo"],"maxbids":4}],"bidderparams":"bar"}}`), }, { - description: "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)", + name: "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo2","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"bidderparams":"bar"}}`), }, { - description: "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)", + name: "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)", bidderParams: map[string]json.RawMessage{bidder: bidderParams}, requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo2","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"bidderparams":"bar"}}`), }, { - description: "Prebid + AlternateBidderCodes.MultiBid.Bidder", + name: "Prebid + AlternateBidderCodes.MultiBid.Bidder", requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`), }, { - description: "Prebid + AlternateBidderCodes.MultiBid.Bidders", + name: "Prebid + AlternateBidderCodes.MultiBid.Bidders", requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic"],"maxbids":4}]}}`), }, { - description: "Prebid + AlternateBidderCodes.MultiBid.Bidder with *", + name: "Prebid + AlternateBidderCodes.MultiBid.Bidder with *", requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`), }, { - description: "Prebid + AlternateBidderCodes.MultiBid.Bidders with *", + name: "Prebid + AlternateBidderCodes.MultiBid.Bidders with *", requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic"],"maxbids":4},{"bidders":["groupm"],"maxbids":4}]}}`), }, { - description: "Prebid + AlternateBidderCodes + MultiBid", + name: "Prebid + AlternateBidderCodes + MultiBid", requestExt: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}},"multibid":[{"bidder":"foo3","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`), expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}}}}`), }, } for _, test := range testCases { - requestExtParsed := &openrtb_ext.ExtRequest{} - if test.requestExt != nil { - err := jsonutil.UnmarshalValid(test.requestExt, requestExtParsed) - if !assert.NoError(t, err, test.description+":parse_ext") { - continue + t.Run(test.name, func(t *testing.T) { + req := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: test.requestExt, + }, } - } + err := buildRequestExtForBidder(bidder, &req, test.bidderParams, test.alternateBidderCodes) + assert.NoError(t, req.RebuildRequest()) + assert.NoError(t, err) - actualJson, actualErr := buildRequestExtForBidder(bidder, test.requestExt, requestExtParsed, test.bidderParams, test.alternateBidderCodes) - if len(test.expectedJson) > 0 { - assert.JSONEq(t, string(test.expectedJson), string(actualJson), test.description+":json") - } else { - assert.Equal(t, test.expectedJson, actualJson, test.description+":json") - } - assert.NoError(t, actualErr, test.description+":err") + if len(test.expectedJson) > 0 { + assert.JSONEq(t, string(test.expectedJson), string(req.Ext)) + } else { + assert.Equal(t, test.expectedJson, req.Ext) + } + }) } } @@ -2502,28 +2739,37 @@ func TestBuildRequestExtForBidder_RequestExtParsedNil(t *testing.T) { var ( bidder = "foo" requestExt = json.RawMessage(`{}`) - requestExtParsed *openrtb_ext.ExtRequest bidderParams map[string]json.RawMessage alternateBidderCodes *openrtb_ext.ExtAlternateBidderCodes ) - actualJson, actualErr := buildRequestExtForBidder(bidder, requestExt, requestExtParsed, bidderParams, alternateBidderCodes) - assert.Nil(t, actualJson) - assert.NoError(t, actualErr) + req := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: requestExt, + }, + } + err := buildRequestExtForBidder(bidder, &req, bidderParams, alternateBidderCodes) + assert.NoError(t, req.RebuildRequest()) + assert.Nil(t, req.Ext) + assert.NoError(t, err) } func TestBuildRequestExtForBidder_RequestExtMalformed(t *testing.T) { var ( bidder = "foo" requestExt = json.RawMessage(`malformed`) - requestExtParsed = &openrtb_ext.ExtRequest{} bidderParams map[string]json.RawMessage alternateBidderCodes *openrtb_ext.ExtAlternateBidderCodes ) - actualJson, actualErr := buildRequestExtForBidder(bidder, requestExt, requestExtParsed, bidderParams, alternateBidderCodes) - assert.Equal(t, json.RawMessage(nil), actualJson) - assert.EqualError(t, actualErr, "expect { or n, but found m") + req := openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: requestExt, + }, + } + err := buildRequestExtForBidder(bidder, &req, bidderParams, alternateBidderCodes) + assert.NoError(t, req.RebuildRequest()) + assert.EqualError(t, err, "expect { or n, but found m") } // newAdapterAliasBidRequest builds a BidRequest with aliases @@ -2709,193 +2955,112 @@ func TestRemoveUnpermissionedEids(t *testing.T) { bidder := "bidderA" testCases := []struct { - description string - userExt json.RawMessage - eidPermissions []openrtb_ext.ExtRequestPrebidDataEidPermission - expectedUserExt json.RawMessage + description string + userEids []openrtb2.EID + eidPermissions []openrtb_ext.ExtRequestPrebidDataEidPermission + expectedUserEids []openrtb2.EID }{ - { - description: "Extension Nil", - userExt: nil, - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"bidderA"}}, - }, - expectedUserExt: nil, - }, - { - description: "Extension Empty", - userExt: json.RawMessage(`{}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"bidderA"}}, - }, - expectedUserExt: json.RawMessage(`{}`), - }, - { - description: "Extension Empty - Keep Other Data", - userExt: json.RawMessage(`{"other":42}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"bidderA"}}, - }, - expectedUserExt: json.RawMessage(`{"other":42}`), - }, + { description: "Eids Empty", - userExt: json.RawMessage(`{"eids":[]}`), + userEids: []openrtb2.EID{}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"bidderA"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[]}`), + expectedUserEids: []openrtb2.EID{}, }, { - description: "Eids Empty - Keep Other Data", - userExt: json.RawMessage(`{"eids":[],"other":42}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"bidderA"}}, - }, - expectedUserExt: json.RawMessage(`{"eids":[],"other":42}`), - }, - { - description: "Allowed By Nil Permissions", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), - eidPermissions: nil, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + description: "Allowed By Nil Permissions", + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, + eidPermissions: nil, + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { - description: "Allowed By Empty Permissions", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{}, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + description: "Allowed By Empty Permissions", + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, + eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{}, + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { description: "Allowed By Specific Bidder", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"bidderA"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { description: "Allowed By Specific Bidder - Case Insensitive", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"BIDDERA"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { description: "Allowed By All Bidders", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"*"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { description: "Allowed By Lack Of Matching Source", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source2", Bidders: []string{"otherBidder"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), - }, - { - description: "Allowed - Keep Other Data", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}],"other":42}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"bidderA"}}, - }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}],"other":42}`), + expectedUserEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, }, { description: "Denied", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}]}`), + userEids: []openrtb2.EID{{Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID"}}}}, eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"otherBidder"}}, }, - expectedUserExt: nil, + expectedUserEids: nil, }, { - description: "Denied - Keep Other Data", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID"}]}],"otherdata":42}`), - eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"otherBidder"}}, + description: "Mix Of Allowed By Specific Bidder, Allowed By Lack Of Matching Source, Denied", + userEids: []openrtb2.EID{ + {Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID1"}}}, + {Source: "source2", UIDs: []openrtb2.UID{{ID: "anyID2"}}}, + {Source: "source3", UIDs: []openrtb2.UID{{ID: "anyID3"}}}, }, - expectedUserExt: json.RawMessage(`{"otherdata":42}`), - }, - { - description: "Mix Of Allowed By Specific Bidder, Allowed By Lack Of Matching Source, Denied, Keep Other Data", - userExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID1"}]},{"source":"source2","uids":[{"id":"anyID2"}]},{"source":"source3","uids":[{"id":"anyID3"}]}],"other":42}`), eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ {Source: "source1", Bidders: []string{"bidderA"}}, {Source: "source3", Bidders: []string{"otherBidder"}}, }, - expectedUserExt: json.RawMessage(`{"eids":[{"source":"source1","uids":[{"id":"anyID1"}]},{"source":"source2","uids":[{"id":"anyID2"}]}],"other":42}`), + expectedUserEids: []openrtb2.EID{ + {Source: "source1", UIDs: []openrtb2.UID{{ID: "anyID1"}}}, + {Source: "source2", UIDs: []openrtb2.UID{{ID: "anyID2"}}}, + }, }, } for _, test := range testCases { - request := &openrtb2.BidRequest{ - User: &openrtb2.User{Ext: test.userExt}, - } + t.Run(test.description, func(t *testing.T) { + request := &openrtb2.BidRequest{ + User: &openrtb2.User{EIDs: test.userEids}, + } - requestExt := &openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ + reqWrapper := openrtb_ext.RequestWrapper{BidRequest: request} + re, _ := reqWrapper.GetRequestExt() + re.SetPrebid(&openrtb_ext.ExtRequestPrebid{ Data: &openrtb_ext.ExtRequestPrebidData{ EidPermissions: test.eidPermissions, }, - }, - } - - expectedRequest := &openrtb2.BidRequest{ - User: &openrtb2.User{Ext: test.expectedUserExt}, - } - - resultErr := removeUnpermissionedEids(request, bidder, requestExt) - assert.NoError(t, resultErr, test.description) - assert.Equal(t, expectedRequest, request, test.description) - } -} + }) -func TestRemoveUnpermissionedEidsUnmarshalErrors(t *testing.T) { - testCases := []struct { - description string - userExt json.RawMessage - expectedErr string - }{ - { - description: "Malformed Ext", - userExt: json.RawMessage(`malformed`), - expectedErr: "expect { or n, but found m", - }, - { - description: "Malformed Eid Array Type", - userExt: json.RawMessage(`{"eids":[42]}`), - expectedErr: "cannot unmarshal []openrtb2.EID: expect { or n, but found 4", - }, - { - description: "Malformed Eid Item Type", - userExt: json.RawMessage(`{"eids":[{"source":42,"id":"anyID"}]}`), - expectedErr: "cannot unmarshal openrtb2.EID.Source: expects \" or n, but found 4", - }, - } - - for _, test := range testCases { - request := &openrtb2.BidRequest{ - User: &openrtb2.User{Ext: test.userExt}, - } - - requestExt := &openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - Data: &openrtb_ext.ExtRequestPrebidData{ - EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"*"}}, - }, - }, - }, - } + expectedRequest := &openrtb2.BidRequest{ + User: &openrtb2.User{EIDs: test.expectedUserEids}, + } - resultErr := removeUnpermissionedEids(request, "bidderA", requestExt) - assert.EqualError(t, resultErr, test.expectedErr, test.description) + resultErr := removeUnpermissionedEids(&reqWrapper, bidder) + assert.NoError(t, resultErr, test.description) + assert.Equal(t, expectedRequest, reqWrapper.BidRequest) + }) } } @@ -3013,23 +3178,17 @@ func TestGetDebugInfo(t *testing.T) { func TestRemoveUnpermissionedEidsEmptyValidations(t *testing.T) { testCases := []struct { - description string - request *openrtb2.BidRequest - requestExt *openrtb_ext.ExtRequest + description string + request *openrtb2.BidRequest + eidPermissions []openrtb_ext.ExtRequestPrebidDataEidPermission }{ { description: "Nil User", request: &openrtb2.BidRequest{ User: nil, }, - requestExt: &openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - Data: &openrtb_ext.ExtRequestPrebidData{ - EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"*"}}, - }, - }, - }, + eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ + {Source: "source1", Bidders: []string{"*"}}, }, }, { @@ -3037,14 +3196,8 @@ func TestRemoveUnpermissionedEidsEmptyValidations(t *testing.T) { request: &openrtb2.BidRequest{ User: &openrtb2.User{}, }, - requestExt: &openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - Data: &openrtb_ext.ExtRequestPrebidData{ - EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ - {Source: "source1", Bidders: []string{"*"}}, - }, - }, - }, + eidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{ + {Source: "source1", Bidders: []string{"*"}}, }, }, { @@ -3052,27 +3205,25 @@ func TestRemoveUnpermissionedEidsEmptyValidations(t *testing.T) { request: &openrtb2.BidRequest{ User: &openrtb2.User{Ext: json.RawMessage(`{"eids":[{"source":"source1","id":"anyID"}]}`)}, }, - requestExt: nil, - }, - { - description: "Nil Prebid Data", - request: &openrtb2.BidRequest{ - User: &openrtb2.User{Ext: json.RawMessage(`{"eids":[{"source":"source1","id":"anyID"}]}`)}, - }, - requestExt: &openrtb_ext.ExtRequest{ - Prebid: openrtb_ext.ExtRequestPrebid{ - Data: nil, - }, - }, }, } for _, test := range testCases { - requestExpected := *test.request + t.Run(test.description, func(t *testing.T) { + requestExpected := *test.request + reqWrapper := openrtb_ext.RequestWrapper{BidRequest: test.request} + + re, _ := reqWrapper.GetRequestExt() + re.SetPrebid(&openrtb_ext.ExtRequestPrebid{ + Data: &openrtb_ext.ExtRequestPrebidData{ + EidPermissions: test.eidPermissions, + }, + }) - resultErr := removeUnpermissionedEids(test.request, "bidderA", test.requestExt) - assert.NoError(t, resultErr, test.description+":err") - assert.Equal(t, &requestExpected, test.request, test.description+":request") + resultErr := removeUnpermissionedEids(&reqWrapper, "bidderA") + assert.NoError(t, resultErr, test.description+":err") + assert.Equal(t, &requestExpected, reqWrapper.BidRequest, test.description+":request") + }) } } @@ -3107,28 +3258,57 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { }, }.Builder + ortb26enabled := config.BidderInfo{OpenRTB: &config.OpenRTBInfo{Version: "2.6"}} reqSplitter := &requestSplitter{ bidderToSyncerKey: map[string]string{}, me: &metrics.MetricsEngineMock{}, privacyConfig: config.Privacy{}, gdprPermsBuilder: gdprPermissionsBuilder, hostSChainNode: nil, - bidderInfo: config.BidderInfos{}, + bidderInfo: config.BidderInfos{"appnexus": ortb26enabled, "axonix": ortb26enabled}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, false, map[string]float64{}) assert.Nil(t, errs) assert.Len(t, bidderRequests, 2, "Bid request count is not 2") - bidRequestSourceExts := map[openrtb_ext.BidderName]json.RawMessage{} + bidRequestSourceSupplyChain := map[openrtb_ext.BidderName]*openrtb2.SupplyChain{} for _, bidderRequest := range bidderRequests { - bidRequestSourceExts[bidderRequest.BidderName] = bidderRequest.BidRequest.Source.Ext + bidRequestSourceSupplyChain[bidderRequest.BidderName] = bidderRequest.BidRequest.Source.SChain + } + + appnexusSchainsSchainExpected := &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, } - appnexusPrebidSchainsSchain := json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`) - axonixPrebidSchainsSchain := json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}`) - assert.Equal(t, appnexusPrebidSchainsSchain, bidRequestSourceExts["appnexus"], "Incorrect appnexus bid request schain in source.ext") - assert.Equal(t, axonixPrebidSchainsSchain, bidRequestSourceExts["axonix"], "Incorrect axonix bid request schain in source.ext") + axonixSchainsSchainExpected := &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller2.com", + SID: "00002", + RID: "BidRequest2", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + } + + assert.Equal(t, appnexusSchainsSchainExpected, bidRequestSourceSupplyChain["appnexus"], "Incorrect appnexus bid request schain ") + assert.Equal(t, axonixSchainsSchainExpected, bidRequestSourceSupplyChain["axonix"], "Incorrect axonix bid request schain") } func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { @@ -3177,7 +3357,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { }}, }, { - description: "bidAjustement Not provided", + description: "bidAdjustment Not provided", gdprAccountEnabled: &falseValue, gdprHostEnabled: true, gdpr: "1", @@ -3219,6 +3399,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: accountConfig, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -3235,7 +3416,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, test.bidAdjustmentFactor) + results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, test.bidAdjustmentFactor) result := results[0] assert.Nil(t, errs) assert.Equal(t, test.expectedImp, result.BidRequest.Imp, test.description) @@ -3251,6 +3432,7 @@ func TestApplyFPD(t *testing.T) { inputBidderIsRequestAlias bool inputRequest openrtb2.BidRequest expectedRequest openrtb2.BidRequest + fpdUserEIDsExisted bool }{ { description: "fpd-nil", @@ -3372,77 +3554,122 @@ func TestApplyFPD(t *testing.T) { inputRequest: openrtb2.BidRequest{}, expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", BuyerUID: "FPDBuyerUID"}}, }, + { + description: "req.User is defined and had bidder fpd user eids (fpdUserEIDsExisted); bidderFPD.User defined and has EIDs. Expect to see user.EIDs in result request taken from fpd", + inputFpd: map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{ + "bidderNormalized": {Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source1"}, {Source: "source2"}}}}, + }, + inputBidderName: "bidderFromRequest", + inputBidderCoreName: "bidderNormalized", + inputBidderIsRequestAlias: false, + inputRequest: openrtb2.BidRequest{User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source3"}, {Source: "source4"}}}}, + expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source1"}, {Source: "source2"}}}}, + fpdUserEIDsExisted: true, + }, + { + description: "req.User is defined and doesn't have fpr user eids (fpdUserEIDsExisted); bidderFPD.User defined and has EIDs. Expect to see user.EIDs in result request taken from original req", + inputFpd: map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{ + "bidderNormalized": {Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source1"}, {Source: "source2"}}}}, + }, + inputBidderName: "bidderFromRequest", + inputBidderCoreName: "bidderNormalized", + inputBidderIsRequestAlias: false, + inputRequest: openrtb2.BidRequest{User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source3"}, {Source: "source4"}}}}, + expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source3"}, {Source: "source4"}}}}, + fpdUserEIDsExisted: false, + }, } for _, testCase := range testCases { - bidderRequest := BidderRequest{ - BidderName: openrtb_ext.BidderName(testCase.inputBidderName), - BidderCoreName: openrtb_ext.BidderName(testCase.inputBidderCoreName), - IsRequestAlias: testCase.inputBidderIsRequestAlias, - BidRequest: &testCase.inputRequest, - } - applyFPD(testCase.inputFpd, bidderRequest) - assert.Equal(t, testCase.expectedRequest, testCase.inputRequest, fmt.Sprintf("incorrect request after applying fpd, testcase %s", testCase.description)) + t.Run(testCase.description, func(t *testing.T) { + reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: &testCase.inputRequest} + applyFPD( + testCase.inputFpd, + openrtb_ext.BidderName(testCase.inputBidderCoreName), + openrtb_ext.BidderName(testCase.inputBidderName), + testCase.inputBidderIsRequestAlias, + reqWrapper, + testCase.fpdUserEIDsExisted, + ) + assert.Equal(t, &testCase.expectedRequest, reqWrapper.BidRequest) + }) } } -func Test_parseAliasesGVLIDs(t *testing.T) { - type args struct { - orig *openrtb2.BidRequest - } +func TestGetRequestAliases(t *testing.T) { tests := []struct { - name string - args args - want map[string]uint16 - wantError bool + name string + givenRequest openrtb_ext.RequestWrapper + wantAliases map[string]string + wantGVLIDs map[string]uint16 + wantError string }{ { - "AliasGVLID Parsed Correctly", - args{ - orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids":{"somealiascode":1}}}`), + name: "nil", + givenRequest: openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + wantAliases: nil, + wantGVLIDs: nil, + wantError: "", + }, + { + name: "empty", + givenRequest: openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), }, }, - map[string]uint16{"somealiascode": 1}, - false, + wantAliases: nil, + wantGVLIDs: nil, + wantError: "", }, { - "AliasGVLID parsing error", - args{ - orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids": {"somealiascode":"abc"}`), + name: "empty-prebid", + givenRequest: openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{}}`), }, }, - nil, - true, + wantAliases: nil, + wantGVLIDs: nil, + wantError: "", }, { - "Invalid AliasGVLID", - args{ - orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}, "aliasgvlids":"abc"}`), + name: "aliases-and-gvlids", + givenRequest: openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"aliases":{"alias1":"bidder1"}, "aliasgvlids":{"alias1":1}}}`), }, }, - nil, - true, + wantAliases: map[string]string{"alias1": "bidder1"}, + wantGVLIDs: map[string]uint16{"alias1": 1}, + wantError: "", }, { - "Missing AliasGVLID", - args{ - orig: &openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"aliases":{"somealiascode":"appnexus"}}`), + name: "malformed", + givenRequest: openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`malformed`), }, }, - nil, - false, + wantAliases: nil, + wantGVLIDs: nil, + wantError: "request.ext is invalid", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseAliasesGVLIDs(tt.args.orig) - assert.Equal(t, tt.want, got, "parseAliasesGVLIDs() got = %v, want %v", got, tt.want) - if !tt.wantError && err != nil { - t.Errorf("parseAliasesGVLIDs() expected error got nil") + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotAliases, gotGVLIDs, err := getRequestAliases(&test.givenRequest) + + assert.Equal(t, test.wantAliases, gotAliases, "aliases") + assert.Equal(t, test.wantGVLIDs, gotGVLIDs, "gvlids") + + if len(test.wantError) > 0 { + require.Len(t, err, 1, "error-len") + assert.EqualError(t, err[0], test.wantError, "error") + } else { + assert.Empty(t, err, "error") } }) } @@ -3661,7 +3888,7 @@ func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, false, map[string]float64{}) assert.Equal(t, test.wantError, len(errs) != 0, test.desc) sort.Slice(bidderRequests, func(i, j int) bool { return bidderRequests[i].BidderCoreName < bidderRequests[j].BidderCoreName @@ -3692,21 +3919,27 @@ func (gs GPPMockSection) Encode(bool) []byte { func TestGdprFromGPP(t *testing.T) { testCases := []struct { name string - initialRequest *openrtb2.BidRequest + initialRequest *openrtb_ext.RequestWrapper gpp gpplib.GppContainer - expectedRequest *openrtb2.BidRequest + expectedRequest *openrtb_ext.RequestWrapper }{ { - name: "Empty", // Empty Request - initialRequest: &openrtb2.BidRequest{}, - gpp: gpplib.GppContainer{}, - expectedRequest: &openrtb2.BidRequest{}, + name: "Empty", // Empty Request + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + gpp: gpplib.GppContainer{}, + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, }, { name: "GDPR_Downgrade", // GDPR from GPP, into empty - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + }, }, }, gpp: gpplib.GppContainer{ @@ -3718,25 +3951,29 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](1), - }, - User: &openrtb2.User{ - Consent: "GDPRConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](1), + }, + User: &openrtb2.User{ + Consent: "GDPRConsent", + }, }, }, }, { name: "GDPR_Downgrade", // GDPR from GPP, into empty legacy, existing objects - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - USPrivacy: "LegacyUSP", - }, - User: &openrtb2.User{ - ID: "1234", + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + USPrivacy: "LegacyUSP", + }, + User: &openrtb2.User{ + ID: "1234", + }, }, }, gpp: gpplib.GppContainer{ @@ -3748,27 +3985,31 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](1), - USPrivacy: "LegacyUSP", - }, - User: &openrtb2.User{ - ID: "1234", - Consent: "GDPRConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](1), + USPrivacy: "LegacyUSP", + }, + User: &openrtb2.User{ + ID: "1234", + Consent: "GDPRConsent", + }, }, }, }, { name: "Downgrade_Blocked_By_Existing", // GDPR from GPP blocked by existing GDPR", - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](1), - }, - User: &openrtb2.User{ - Consent: "LegacyConsent", + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](1), + }, + User: &openrtb2.User{ + Consent: "LegacyConsent", + }, }, }, gpp: gpplib.GppContainer{ @@ -3780,22 +4021,26 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](1), - }, - User: &openrtb2.User{ - Consent: "LegacyConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](1), + }, + User: &openrtb2.User{ + Consent: "LegacyConsent", + }, }, }, }, { name: "Downgrade_Partial", // GDPR from GPP partially blocked by existing GDPR - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](0), + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](0), + }, }, }, gpp: gpplib.GppContainer{ @@ -3807,21 +4052,25 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, - GDPR: ptrutil.ToPtr[int8](0), - }, - User: &openrtb2.User{ - Consent: "GDPRConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + GDPR: ptrutil.ToPtr[int8](0), + }, + User: &openrtb2.User{ + Consent: "GDPRConsent", + }, }, }, }, { name: "No_GDPR", // Downgrade not possible due to missing GDPR - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + }, }, }, gpp: gpplib.GppContainer{ @@ -3833,18 +4082,22 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, - GDPR: ptrutil.ToPtr[int8](0), + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + GDPR: ptrutil.ToPtr[int8](0), + }, }, }, }, { name: "No_SID", // GDPR from GPP partially blocked by no SID - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + }, }, }, gpp: gpplib.GppContainer{ @@ -3860,19 +4113,23 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, - GDPR: ptrutil.ToPtr[int8](0), - }, - User: &openrtb2.User{ - Consent: "GDPRConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + GDPR: ptrutil.ToPtr[int8](0), + }, + User: &openrtb2.User{ + Consent: "GDPRConsent", + }, }, }, }, { - name: "GDPR_Nil_SID", // GDPR from GPP, into empty, but with nil SID - initialRequest: &openrtb2.BidRequest{}, + name: "GDPR_Nil_SID", // GDPR from GPP, into empty, but with nil SID + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, gpp: gpplib.GppContainer{ SectionTypes: []constants.SectionID{2}, Sections: []gpplib.Section{ @@ -3882,20 +4139,24 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - User: &openrtb2.User{ - Consent: "GDPRConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ + Consent: "GDPRConsent", + }, }, }, }, { name: "Downgrade_Nil_SID_Blocked_By_Existing", // GDPR from GPP blocked by existing GDPR, with nil SID", - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GDPR: ptrutil.ToPtr[int8](1), - }, - User: &openrtb2.User{ - Consent: "LegacyConsent", + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GDPR: ptrutil.ToPtr[int8](1), + }, + User: &openrtb2.User{ + Consent: "LegacyConsent", + }, }, }, gpp: gpplib.GppContainer{ @@ -3907,12 +4168,14 @@ func TestGdprFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GDPR: ptrutil.ToPtr[int8](1), - }, - User: &openrtb2.User{ - Consent: "LegacyConsent", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GDPR: ptrutil.ToPtr[int8](1), + }, + User: &openrtb2.User{ + Consent: "LegacyConsent", + }, }, }, }, @@ -3929,21 +4192,27 @@ func TestGdprFromGPP(t *testing.T) { func TestPrivacyFromGPP(t *testing.T) { testCases := []struct { name string - initialRequest *openrtb2.BidRequest + initialRequest *openrtb_ext.RequestWrapper gpp gpplib.GppContainer - expectedRequest *openrtb2.BidRequest + expectedRequest *openrtb_ext.RequestWrapper }{ { - name: "Empty", // Empty Request - initialRequest: &openrtb2.BidRequest{}, - gpp: gpplib.GppContainer{}, - expectedRequest: &openrtb2.BidRequest{}, + name: "Empty", // Empty Request + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + gpp: gpplib.GppContainer{}, + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, }, { name: "Privacy_Downgrade", // US Privacy from GPP, into empty - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + }, }, }, gpp: gpplib.GppContainer{ @@ -3955,19 +4224,23 @@ func TestPrivacyFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, - USPrivacy: "USPrivacy", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + USPrivacy: "USPrivacy", + }, }, }, }, { name: "Downgrade_Blocked_By_Existing", // US Privacy from GPP blocked by existing US Privacy - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, - USPrivacy: "LegacyPrivacy", + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + USPrivacy: "LegacyPrivacy", + }, }, }, gpp: gpplib.GppContainer{ @@ -3979,18 +4252,22 @@ func TestPrivacyFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{6}, - USPrivacy: "LegacyPrivacy", + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{6}, + USPrivacy: "LegacyPrivacy", + }, }, }, }, { name: "No_USPrivacy", // Downgrade not possible due to missing USPrivacy - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + }, }, }, gpp: gpplib.GppContainer{ @@ -4002,17 +4279,21 @@ func TestPrivacyFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + }, }, }, }, { name: "No_SID", // US Privacy from GPP partially blocked by no SID - initialRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, + initialRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + }, }, }, gpp: gpplib.GppContainer{ @@ -4028,9 +4309,11 @@ func TestPrivacyFromGPP(t *testing.T) { }, }, }, - expectedRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - GPPSID: []int8{2}, + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + GPPSID: []int8{2}, + }, }, }, }, @@ -4520,8 +4803,10 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { privacyConfig config.AccountPrivacy componentName string allow bool + ortbVersion string expectedReqNumber int expectedUser openrtb2.User + expectUserScrub bool expectedDevice openrtb2.Device expectedSource openrtb2.Source expectedImpExt json.RawMessage @@ -4530,6 +4815,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "fetch_bids_request_with_one_bidder_allowed", req: newBidRequest(t), privacyConfig: getFetchBidsActivityConfig("appnexus", true), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: expectedUserDefault, expectedDevice: expectedDeviceDefault, @@ -4548,6 +4834,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "transmit_ufpd_allowed", req: newBidRequest(t), privacyConfig: getTransmitUFPDActivityConfig("appnexus", true), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: expectedUserDefault, expectedDevice: expectedDeviceDefault, @@ -4569,6 +4856,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { Ext: json.RawMessage(`{"test":2}`), Data: nil, }, + expectUserScrub: true, expectedDevice: openrtb2.Device{ UA: deviceUA, Language: "EN", @@ -4588,6 +4876,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "transmit_precise_geo_allowed", req: newBidRequest(t), privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", true), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: expectedUserDefault, expectedDevice: expectedDeviceDefault, @@ -4599,6 +4888,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "transmit_precise_geo_deny", req: newBidRequest(t), privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", false), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: openrtb2.User{ ID: "our-id", @@ -4631,6 +4921,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "transmit_tid_allowed", req: newBidRequest(t), privacyConfig: getTransmitTIDActivityConfig("appnexus", true), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: expectedUserDefault, expectedDevice: expectedDeviceDefault, @@ -4641,6 +4932,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { name: "transmit_tid_deny", req: newBidRequest(t), privacyConfig: getTransmitTIDActivityConfig("appnexus", false), + ortbVersion: "2.6", expectedReqNumber: 1, expectedUser: expectedUserDefault, expectedDevice: expectedDeviceDefault, @@ -4666,17 +4958,21 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { AnonKeepBits: 16, }, }}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } + metricsMock := metrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordAdapterBuyerUIDScrubbed", mock.Anything).Return() + bidderToSyncerKey := map[string]string{} reqSplitter := &requestSplitter{ bidderToSyncerKey: bidderToSyncerKey, - me: &metrics.MetricsEngineMock{}, + me: &metricsMock, hostSChainNode: nil, - bidderInfo: config.BidderInfos{}, + bidderInfo: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{Version: test.ortbVersion}}}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, false, map[string]float64{}) assert.Empty(t, errs) assert.Len(t, bidderRequests, test.expectedReqNumber) @@ -4688,6 +4984,11 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { if len(test.expectedImpExt) > 0 { assert.JSONEq(t, string(test.expectedImpExt), string(bidderRequests[0].BidRequest.Imp[0].Ext)) } + if test.expectUserScrub { + metricsMock.AssertCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) + } else { + metricsMock.AssertNotCalled(t, "RecordAdapterBuyerUIDScrubbed", openrtb_ext.BidderAppnexus) + } } }) } @@ -4742,119 +5043,96 @@ func getTransmitTIDActivityConfig(componentName string, allow bool) config.Accou func TestApplyBidAdjustmentToFloor(t *testing.T) { type args struct { - allBidderRequests []BidderRequest + bidRequestWrapper *openrtb_ext.RequestWrapper + bidderName string bidAdjustmentFactors map[string]float64 } tests := []struct { - name string - args args - expectedAllBidderRequests []BidderRequest + name string + args args + expectedBidRequest *openrtb2.BidRequest }{ { - name: " bidAdjustmentFactor is empty", + name: "bid_adjustment_factor_is_nil", args: args{ - allBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), - }, - }, - bidAdjustmentFactors: map[string]float64{}, - }, - expectedAllBidderRequests: []BidderRequest{ - { + bidRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, - BidderName: openrtb_ext.BidderName("appnexus"), }, + bidderName: "appnexus", + bidAdjustmentFactors: nil, + }, + expectedBidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, }, { - name: "bidAdjustmentFactor not present for request bidder", + name: "bid_adjustment_factor_is_empty", args: args{ - allBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, }, - bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0}, + bidderName: "appnexus", + bidAdjustmentFactors: map[string]float64{}, }, - expectedAllBidderRequests: []BidderRequest{ - { + expectedBidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + }, + { + name: "bid_adjustment_factor_not_present", + args: args{ + bidRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, - BidderName: openrtb_ext.BidderName("appnexus"), }, + bidderName: "appnexus", + bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0}, + }, + expectedBidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, }, { - name: "bidAdjustmentFactor present for request bidder", + name: "bid_adjustment_factor_present", args: args{ - allBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, }, + bidderName: "appnexus", bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0, "appnexus": 0.75}, }, - expectedAllBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 133.33333333333334}, {BidFloor: 200}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), - }, + expectedBidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 133.33333333333334}, {BidFloor: 200}}, }, }, { - name: "bidAdjustmentFactor present only for appnexus request bidder", + name: "bid_adjustment_factor_present_and_zero", args: args{ - allBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), - }, - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, - }, - BidderName: openrtb_ext.BidderName("pubmatic"), - }, - }, - bidAdjustmentFactors: map[string]float64{"appnexus": 0.75}, - }, - expectedAllBidderRequests: []BidderRequest{ - { - BidRequest: &openrtb2.BidRequest{ - Imp: []openrtb2.Imp{{BidFloor: 133.33333333333334}, {BidFloor: 200}}, - }, - BidderName: openrtb_ext.BidderName("appnexus"), - }, - { + bidRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, - BidderName: openrtb_ext.BidderName("pubmatic"), }, + bidderName: "appnexus", + bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0, "appnexus": 0.0}, + }, + expectedBidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - applyBidAdjustmentToFloor(tt.args.allBidderRequests, tt.args.bidAdjustmentFactors) - assert.Equal(t, tt.expectedAllBidderRequests, tt.args.allBidderRequests, tt.name) + applyBidAdjustmentToFloor(tt.args.bidRequestWrapper, tt.args.bidderName, tt.args.bidAdjustmentFactors) + assert.NoError(t, tt.args.bidRequestWrapper.RebuildRequest()) + assert.Equal(t, tt.expectedBidRequest, tt.args.bidRequestWrapper.BidRequest, tt.name) }) } } @@ -5077,24 +5355,85 @@ func TestCopyExtAlternateBidderCodes(t *testing.T) { } } -func TestBuildBidResponseRequestBidderName(t *testing.T) { - bidderImpResponses := stored_responses.BidderImpsWithBidResponses{ - openrtb_ext.BidderName("appnexus"): {"impId1": json.RawMessage(`{}`), "impId2": json.RawMessage(`{}`)}, - openrtb_ext.BidderName("appneXUS"): {"impId3": json.RawMessage(`{}`), "impId4": json.RawMessage(`{}`)}, - } +func TestRemoveImpsWithStoredResponses(t *testing.T) { + bidRespId1 := json.RawMessage(`{"id": "resp_id1"}`) + testCases := []struct { + description string + req *openrtb_ext.RequestWrapper + storedBidResponses map[string]json.RawMessage + expectedImps []openrtb2.Imp + }{ + { + description: "request with imps and stored bid response for this imp", + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp-id1"}, + }, + }, + }, + storedBidResponses: map[string]json.RawMessage{ + "imp-id1": bidRespId1, + }, + expectedImps: nil, + }, + { + description: "request with imps and stored bid response for one of these imp", + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp-id1"}, + {ID: "imp-id2"}, + }, + }, + }, + storedBidResponses: map[string]json.RawMessage{ + "imp-id1": bidRespId1, + }, + expectedImps: []openrtb2.Imp{ + { + ID: "imp-id2", + }, + }, + }, + { + description: "request with imps and stored bid response for both of these imp", + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp-id1"}, + {ID: "imp-id2"}, + }, + }, + }, + storedBidResponses: map[string]json.RawMessage{ + "imp-id1": bidRespId1, + "imp-id2": bidRespId1, + }, + expectedImps: nil, + }, + { + description: "request with imps and no stored bid responses", + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp-id1"}, + {ID: "imp-id2"}, + }, + }, + }, + storedBidResponses: nil, - bidderImpReplaceImpID := stored_responses.BidderImpReplaceImpID{ - "appnexus": {"impId1": true, "impId2": false}, - "appneXUS": {"impId3": true, "impId4": false}, + expectedImps: []openrtb2.Imp{ + {ID: "imp-id1"}, + {ID: "imp-id2"}, + }, + }, + } + for _, testCase := range testCases { + request := testCase.req + removeImpsWithStoredResponses(request, testCase.storedBidResponses) + assert.NoError(t, request.RebuildRequest()) + assert.Equal(t, testCase.expectedImps, request.Imp, "incorrect Impressions for testCase %s", testCase.description) } - result := buildBidResponseRequest(nil, bidderImpResponses, nil, bidderImpReplaceImpID) - - resultAppnexus := result["appnexus"] - assert.Equal(t, resultAppnexus.BidderName, openrtb_ext.BidderName("appnexus")) - assert.Equal(t, resultAppnexus.ImpReplaceImpId, map[string]bool{"impId1": true, "impId2": false}) - - resultAppneXUS := result["appneXUS"] - assert.Equal(t, resultAppneXUS.BidderName, openrtb_ext.BidderName("appneXUS")) - assert.Equal(t, resultAppneXUS.ImpReplaceImpId, map[string]bool{"impId3": true, "impId4": false}) - } diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 1b4f6cb4680..4f7df2f3ab7 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -20,8 +20,8 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // - // If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent. - AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) + // If the consent string was nonsensical, the no permissions are granted. + AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions } type PermissionsBuilder func(TCF2ConfigReader, RequestInfo) Permissions diff --git a/gdpr/impl.go b/gdpr/impl.go index fd3ad2b2dd9..d364883b91a 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -56,33 +56,36 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ } // AuctionActivitiesAllowed determines whether auction activities are permitted for a given bidder -func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) { +func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions { if _, ok := p.nonStandardPublishers[p.publisherID]; ok { - return AllowAll, nil + return AllowAll } + if p.gdprSignal != SignalYes { - return AllowAll, nil + return AllowAll } + if p.consent == "" { - return p.defaultPermissions(), nil + return p.defaultPermissions() } + pc, err := parseConsent(p.consent) if err != nil { - return p.defaultPermissions(), err + return p.defaultPermissions() } + vendorID, _ := p.resolveVendorID(bidderCoreName, bidder) vendor, err := p.getVendor(ctx, vendorID, *pc) if err != nil { - return p.defaultPermissions(), err + return p.defaultPermissions() } - vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor} - - permissions = AuctionPermissions{} - permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo) - permissions.PassGeo = p.allowGeo(bidderCoreName, pc.consentMeta, vendor) - permissions.PassID = p.allowID(bidderCoreName, pc.consentMeta, vendorInfo) - return permissions, nil + vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor} + return AuctionPermissions{ + AllowBidRequest: p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo), + PassGeo: p.allowGeo(bidderCoreName, pc.consentMeta, vendor), + PassID: p.allowID(bidderCoreName, pc.consentMeta, vendorInfo), + } } // defaultPermissions returns a permissions object that denies passing user IDs while @@ -222,6 +225,6 @@ func (a AlwaysAllow) HostCookiesAllowed(ctx context.Context) (bool, error) { func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) { return true, nil } -func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) { - return AllowAll, nil +func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions { + return AllowAll } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index fc3d69d9c57..928d4fe8e82 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -335,9 +335,8 @@ func TestAllowActivities(t *testing.T) { perms.gdprSignal = tt.gdpr perms.publisherID = tt.publisherID - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderCoreName, tt.bidderName) + permissions := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderCoreName, tt.bidderName) - assert.Nil(t, err, tt.description) assert.Equal(t, tt.passID, permissions.PassID, tt.description) } } @@ -437,8 +436,7 @@ func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), } - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID) - assert.NoError(t, err) + permissions := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID) assert.Equal(t, tt.allowBidRequest, permissions.AllowBidRequest) assert.Equal(t, tt.passID, permissions.PassID) }) @@ -658,8 +656,7 @@ func TestAllowActivitiesGeoAndID(t *testing.T) { perms.consent = td.consent perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) - assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) + permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) @@ -695,8 +692,7 @@ func TestAllowActivitiesWhitelist(t *testing.T) { } // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, openrtb_ext.BidderAppnexus) - assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed") + permissions := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, openrtb_ext.BidderAppnexus) assert.EqualValuesf(t, true, permissions.PassGeo, "PassGeo failure") assert.EqualValuesf(t, true, permissions.PassID, "PassID failure") } @@ -767,8 +763,7 @@ func TestAllowActivitiesPubRestrict(t *testing.T) { perms.aliasGVLIDs = td.aliasGVLIDs perms.consent = td.consent - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) - assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) + permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) } @@ -1101,8 +1096,7 @@ func TestAllowActivitiesBidRequests(t *testing.T) { perms.cfg = &tcf2AggConfig perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) - assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) + permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) @@ -1195,8 +1189,7 @@ func TestAllowActivitiesVendorException(t *testing.T) { perms.cfg = &tcf2AggConfig perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) - assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) + permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) diff --git a/go.mod b/go.mod index 94e2a2a4e3c..090a39357cf 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 - github.com/rs/cors v1.8.2 + github.com/rs/cors v1.11.0 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.8.1 github.com/vrischmann/go-metrics-influxdb v0.1.1 @@ -41,6 +41,7 @@ require ( ) require ( + github.com/51Degrees/device-detection-go/v4 v4.4.35 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -66,6 +67,10 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect diff --git a/go.sum b/go.sum index bed6194599a..80b58cd7c33 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/51Degrees/device-detection-go/v4 v4.4.35 h1:qhP2tzoXhGE1aYY3NftMJ+ccxz0+2kM8aF4SH7fTyuA= +github.com/51Degrees/device-detection-go/v4 v4.4.35/go.mod h1:dbdG1fySqdY+a5pUnZ0/G0eD03G6H3Vh8kRC+1f9qSc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -135,6 +137,7 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -315,9 +318,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= @@ -435,8 +440,9 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -482,6 +488,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/vrischmann/go-metrics-influxdb v0.1.1 h1:xneKFRjsS4BiVYvAKaM/rOlXYd1pGHksnES0ECCJLgo= github.com/vrischmann/go-metrics-influxdb v0.1.1/go.mod h1:q7YC8bFETCYopXRMtUvQQdLaoVhpsEwvQS2zZEYCqg8= diff --git a/injector/injector_test.go b/injector/injector_test.go new file mode 100644 index 00000000000..1a6385c48e9 --- /dev/null +++ b/injector/injector_test.go @@ -0,0 +1,455 @@ +package injector + +import ( + "errors" + "strings" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/exchange/entities" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +var reqWrapper = &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + Site: &openrtb2.Site{ + Domain: "testdomain", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + Page: "pageurltest", + }, + App: &openrtb2.App{ + Domain: "testdomain", + Bundle: "testbundle", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + }, + Device: &openrtb2.Device{ + Lmt: ptrutil.ToPtr(int8(1)), + }, + User: &openrtb2.User{Consent: "1", Ext: []byte(`{"consent":"2" }`)}, + Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1"}}}`), + }, +} + +func TestInjectTracker(t *testing.T) { + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Errors: []string{"http://errortracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + Impressions: []string{"http://impressiontracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + VideoClicks: []string{"http://videoclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + NonLinearClickTracking: []string{"http://nonlinearclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + CompanionClickThrough: []string{"http://companionclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + type args struct { + vastXML string + NURL string + } + tests := []struct { + name string + args args + want string + wantError error + }{ + { + name: "Empty vastXML and NURL present", + args: args{ + vastXML: "", + NURL: "www.nurl.com", + }, + want: `prebid.org wrapper`, + wantError: nil, + }, + { + name: "Empty vastXML and empty NURL", + args: args{ + vastXML: "", + NURL: "", + }, + want: "", + wantError: errors.New("invalid Vast XML"), + }, + { + name: "No Inline/Wrapper tag present", + args: args{ + vastXML: ``, + NURL: "", + }, + want: ``, + wantError: errors.New("invalid VastXML, inline/wrapper tag not found"), + }, + { + name: "Invalid Vast XML, parsing error", + args: args{ + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressionInline Simple AdIAB Sample CompanyAD CONTENT description category846500:00:16`, + NURL: "", + }, + want: ``, + wantError: errors.New("XML processing error: xml: end tag does not match start tag "), + }, + { + name: "Inline Linear vastXML, no existing event tracker", + args: args{ + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressionInline Simple AdIAB Sample CompanyAD CONTENT description category846500:00:16`, + NURL: "", + }, + want: ``, + }, + { + name: "Non Linear vastXML, no existing event tracker", + args: args{ + NURL: "", + vastXML: `iabtechlab8465`, + }, + want: ``, + }, + { + name: "Wrapper Liner vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impression`, + }, + want: ``, + }, + { + name: "Wapper companion vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlab`, + }, + want: ``, + }, + { + name: "Wapper no companion vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlab`, + }, + want: ``, + }, + { + name: "Inline Non Linear empty", + args: args{ + NURL: "", + vastXML: `iabtechlaba532d16d-4d7f-4440-bd29-2ec0e693fc80iabtechlab video ad`, + }, + want: ``, + }, + { + name: "Wrapper linear and non linear", + args: args{ + NURL: "", + vastXML: `Test Ad Server`, + }, + want: ``, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ti.InjectTracker(tt.args.vastXML, tt.args.NURL) + assert.Equal(t, tt.want, got, tt.name) + if tt.wantError != nil { + assert.EqualError(t, err, tt.wantError.Error()) + } + }) + } +} + +func TestAddClickTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + VideoClicks: []string{"http://videoclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addClickTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddImpressionTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "Add impression tag", + addParentTag: true, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Impressions: []string{"http://impressiontracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addImpressionTrackingEvent(&outputXML) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddErrorTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "Add impression tag", + addParentTag: true, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Errors: []string{"http://errortracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addErrorTrackingEvent(&outputXML) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddNonLinearClickTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + NonLinearClickTracking: []string{"http://nonlinearclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addNonLinearClickTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddCompanionClickThroughEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + CompanionClickThrough: []string{"http://companionclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addCompanionClickThroughEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + ti.addTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestWriteTrackingEvent(t *testing.T) { + tests := []struct { + name string + urls []string + startTag string + endTag string + creativeId string + eventType string + vastEvent string + expectedXML string + }{ + { + name: "Single URL", + urls: []string{"http://tracker.com"}, + startTag: "", + endTag: "", + creativeId: "123", + eventType: "start", + vastEvent: "tracking", + expectedXML: "http://tracker.com", + }, + { + name: "Multiple URL", + urls: []string{"http://tracker1.com", "http://tracker2.com"}, + startTag: "", + endTag: "", + creativeId: "123", + eventType: "start", + vastEvent: "tracking", + expectedXML: "http://tracker1.comhttp://tracker2.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + ti.writeTrackingEvent(tt.urls, &outputXML, tt.startTag, tt.endTag, tt.creativeId, tt.eventType, tt.vastEvent) + assert.Equal(t, tt.expectedXML, outputXML.String(), tt.name) + }) + } +} diff --git a/macros/macros.go b/macros/macros.go index bde843c3cbb..2b0e29d6238 100644 --- a/macros/macros.go +++ b/macros/macros.go @@ -17,6 +17,10 @@ type EndpointTemplateParams struct { GvlID string PageID string SupplyId string + SspId string + SspID string + SeatID string + TokenID string } // UserSyncPrivacy specifies privacy policy macros, represented as strings, for user sync urls. diff --git a/macros/provider.go b/macros/provider.go index 3cae540e22a..29b8836bcd2 100644 --- a/macros/provider.go +++ b/macros/provider.go @@ -104,9 +104,8 @@ func (b *macroProvider) populateRequestMacros(reqWrapper *openrtb_ext.RequestWra } } - userExt, err := reqWrapper.GetUserExt() - if err == nil && userExt != nil && userExt.GetConsent() != nil { - b.macros[MacroKeyConsent] = *userExt.GetConsent() + if reqWrapper.User != nil && len(reqWrapper.User.Consent) > 0 { + b.macros[MacroKeyConsent] = reqWrapper.User.Consent } if reqWrapper.Device != nil && reqWrapper.Device.Lmt != nil { b.macros[MacroKeyLmtTracking] = strconv.Itoa(int(*reqWrapper.Device.Lmt)) diff --git a/macros/provider_test.go b/macros/provider_test.go index b3f5c9a88a9..a560ee80bc0 100644 --- a/macros/provider_test.go +++ b/macros/provider_test.go @@ -132,7 +132,7 @@ func TestPopulateRequestMacros(t *testing.T) { args: args{ reqWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ - User: &openrtb2.User{Ext: []byte(`{"consent":"1" }`)}, + User: &openrtb2.User{Consent: "1", Ext: []byte(`{"consent":"2" }`)}, Ext: []byte(`{"prebid":{"integration":"testIntegration"}}`), }, }, @@ -189,7 +189,7 @@ func TestPopulateRequestMacros(t *testing.T) { Device: &openrtb2.Device{ Lmt: &lmt, }, - User: &openrtb2.User{Ext: []byte(`{"consent":"1" }`)}, + User: &openrtb2.User{Consent: "1", Ext: []byte(`{"consent":"2" }`)}, Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1"}}}`), }, }, diff --git a/macros/string_index_based_replacer_test.go b/macros/string_index_based_replacer_test.go index eb81a1520e9..d8d0971de31 100644 --- a/macros/string_index_based_replacer_test.go +++ b/macros/string_index_based_replacer_test.go @@ -123,7 +123,7 @@ var req *openrtb_ext.RequestWrapper = &openrtb_ext.RequestWrapper{ Device: &openrtb2.Device{ Lmt: &lmt, }, - User: &openrtb2.User{Ext: []byte(`{"consent":"yes" }`)}, + User: &openrtb2.User{Consent: "yes", Ext: []byte(`{"consent":"no" }`)}, Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1","CUSTOMMACR2":"value2","CUSTOMMACR3":"value3"}}}`), }, } diff --git a/metrics/config/metrics_test.go b/metrics/config/metrics_test.go index 5badc348e61..f6a4f192224 100644 --- a/metrics/config/metrics_test.go +++ b/metrics/config/metrics_test.go @@ -39,17 +39,14 @@ func TestGoMetricsEngine(t *testing.T) { } } -// Test the multiengine func TestMultiMetricsEngine(t *testing.T) { cfg := mainConfig.Configuration{} cfg.Metrics.Influxdb.Host = "localhost" adapterList := openrtb_ext.CoreBidderNames() goEngine := metrics.NewMetrics(gometrics.NewPrefixedRegistry("prebidserver."), adapterList, mainConfig.DisabledMetrics{}, nil, modulesStages) - engineList := make(MultiMetricsEngine, 2) - engineList[0] = goEngine - engineList[1] = &NilMetricsEngine{} - var metricsEngine metrics.MetricsEngine - metricsEngine = &engineList + metricsEngine := make(MultiMetricsEngine, 2) + metricsEngine[0] = goEngine + metricsEngine[1] = &NilMetricsEngine{} labels := metrics.Labels{ Source: metrics.DemandWeb, RType: metrics.ReqTypeORTB2Web, @@ -109,23 +106,23 @@ func TestMultiMetricsEngine(t *testing.T) { metricsEngine.RecordModuleExecutionError(module) metricsEngine.RecordModuleTimeout(module) } - labelsBlacklist := []metrics.Labels{ + labelsBlocked := []metrics.Labels{ { Source: metrics.DemandWeb, RType: metrics.ReqTypeAMP, PubID: "test2", CookieFlag: metrics.CookieFlagYes, - RequestStatus: metrics.RequestStatusBlacklisted, + RequestStatus: metrics.RequestStatusBlockedApp, }, { Source: metrics.DemandWeb, RType: metrics.ReqTypeVideo, PubID: "test2", CookieFlag: metrics.CookieFlagYes, - RequestStatus: metrics.RequestStatusBlacklisted, + RequestStatus: metrics.RequestStatusBlockedApp, }, } - for _, label := range labelsBlacklist { + for _, label := range labelsBlocked { metricsEngine.RecordRequest(label) } impTypeLabels.BannerImps = false @@ -143,6 +140,7 @@ func TestMultiMetricsEngine(t *testing.T) { metricsEngine.RecordStoredImpCacheResult(metrics.CacheHit, 5) metricsEngine.RecordAccountCacheResult(metrics.CacheHit, 6) + metricsEngine.RecordAdapterBuyerUIDScrubbed(openrtb_ext.BidderAppnexus) metricsEngine.RecordAdapterGDPRRequestBlocked(openrtb_ext.BidderAppnexus) metricsEngine.RecordRequestQueueTime(false, metrics.ReqTypeVideo, time.Duration(1)) @@ -150,14 +148,14 @@ func TestMultiMetricsEngine(t *testing.T) { //Make the metrics engine, instantiated here with goEngine, fill its RequestStatuses[RequestType][metrics.RequestStatusXX] with the new boolean values added to metrics.Labels VerifyMetrics(t, "RequestStatuses.OpenRTB2.OK", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusOK].Count(), 5) VerifyMetrics(t, "RequestStatuses.AMP.OK", goEngine.RequestStatuses[metrics.ReqTypeAMP][metrics.RequestStatusOK].Count(), 0) - VerifyMetrics(t, "RequestStatuses.AMP.BlacklistedAcctOrApp", goEngine.RequestStatuses[metrics.ReqTypeAMP][metrics.RequestStatusBlacklisted].Count(), 1) + VerifyMetrics(t, "RequestStatuses.AMP.BlockedApp", goEngine.RequestStatuses[metrics.ReqTypeAMP][metrics.RequestStatusBlockedApp].Count(), 1) VerifyMetrics(t, "RequestStatuses.Video.OK", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusOK].Count(), 0) VerifyMetrics(t, "RequestStatuses.Video.Error", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusErr].Count(), 0) VerifyMetrics(t, "RequestStatuses.Video.BadInput", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusBadInput].Count(), 0) - VerifyMetrics(t, "RequestStatuses.Video.BlacklistedAcctOrApp", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusBlacklisted].Count(), 1) + VerifyMetrics(t, "RequestStatuses.Video.BlockedApp", goEngine.RequestStatuses[metrics.ReqTypeVideo][metrics.RequestStatusBlockedApp].Count(), 1) VerifyMetrics(t, "RequestStatuses.OpenRTB2.Error", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusErr].Count(), 0) VerifyMetrics(t, "RequestStatuses.OpenRTB2.BadInput", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusBadInput].Count(), 0) - VerifyMetrics(t, "RequestStatuses.OpenRTB2.BlacklistedAcctOrApp", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusBlacklisted].Count(), 0) + VerifyMetrics(t, "RequestStatuses.OpenRTB2.BlockedApp", goEngine.RequestStatuses[metrics.ReqTypeORTB2Web][metrics.RequestStatusBlockedApp].Count(), 0) VerifyMetrics(t, "ImpsTypeBanner", goEngine.ImpsTypeBanner.Count(), 5) VerifyMetrics(t, "ImpsTypeVideo", goEngine.ImpsTypeVideo.Count(), 3) @@ -188,6 +186,7 @@ func TestMultiMetricsEngine(t *testing.T) { VerifyMetrics(t, "StoredImpCache.Hit", goEngine.StoredImpCacheMeter[metrics.CacheHit].Count(), 5) VerifyMetrics(t, "AccountCache.Hit", goEngine.AccountCacheMeter[metrics.CacheHit].Count(), 6) + VerifyMetrics(t, "AdapterMetrics.appNexus.BuyerUIDScrubbed", goEngine.AdapterMetrics[strings.ToLower(string(openrtb_ext.BidderAppnexus))].BuyerUIDScrubbed.Count(), 1) VerifyMetrics(t, "AdapterMetrics.appNexus.GDPRRequestBlocked", goEngine.AdapterMetrics[strings.ToLower(string(openrtb_ext.BidderAppnexus))].GDPRRequestBlocked.Count(), 1) // verify that each module has its own metric recorded diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go index 05529220f16..9ca6505aa9e 100644 --- a/metrics/go_metrics_test.go +++ b/metrics/go_metrics_test.go @@ -73,7 +73,7 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "syncer.foo.request.ok", m.SyncerRequestsMeter["foo"][SyncerCookieSyncOK]) ensureContains(t, registry, "syncer.foo.request.privacy_blocked", m.SyncerRequestsMeter["foo"][SyncerCookieSyncPrivacyBlocked]) ensureContains(t, registry, "syncer.foo.request.already_synced", m.SyncerRequestsMeter["foo"][SyncerCookieSyncAlreadySynced]) - ensureContains(t, registry, "syncer.foo.request.type_not_supported", m.SyncerRequestsMeter["foo"][SyncerCookieSyncTypeNotSupported]) + ensureContains(t, registry, "syncer.foo.request.rejected_by_filter", m.SyncerRequestsMeter["foo"][SyncerCookieSyncRejectedByFilter]) ensureContains(t, registry, "syncer.foo.set.ok", m.SyncerSetsMeter["foo"][SyncerSetUidOK]) ensureContains(t, registry, "syncer.foo.set.cleared", m.SyncerSetsMeter["foo"][SyncerSetUidCleared]) @@ -864,7 +864,7 @@ func TestRecordSyncerRequest(t *testing.T) { assert.Equal(t, m.SyncerRequestsMeter["foo"][SyncerCookieSyncOK].Count(), int64(1)) assert.Equal(t, m.SyncerRequestsMeter["foo"][SyncerCookieSyncPrivacyBlocked].Count(), int64(0)) assert.Equal(t, m.SyncerRequestsMeter["foo"][SyncerCookieSyncAlreadySynced].Count(), int64(0)) - assert.Equal(t, m.SyncerRequestsMeter["foo"][SyncerCookieSyncTypeNotSupported].Count(), int64(0)) + assert.Equal(t, m.SyncerRequestsMeter["foo"][SyncerCookieSyncRejectedByFilter].Count(), int64(0)) } func TestRecordSetUid(t *testing.T) { diff --git a/metrics/metrics.go b/metrics/metrics.go index 7d3dc819341..c254fd3ee16 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -234,7 +234,7 @@ const ( RequestStatusBadInput RequestStatus = "badinput" RequestStatusErr RequestStatus = "err" RequestStatusNetworkErr RequestStatus = "networkerr" - RequestStatusBlacklisted RequestStatus = "blacklistedacctorapp" + RequestStatusBlockedApp RequestStatus = "blockedapp" RequestStatusQueueTimeout RequestStatus = "queuetimeout" RequestStatusAccountConfigErr RequestStatus = "acctconfigerr" ) @@ -245,7 +245,7 @@ func RequestStatuses() []RequestStatus { RequestStatusBadInput, RequestStatusErr, RequestStatusNetworkErr, - RequestStatusBlacklisted, + RequestStatusBlockedApp, RequestStatusQueueTimeout, RequestStatusAccountConfigErr, } @@ -361,7 +361,7 @@ const ( SyncerCookieSyncOK SyncerCookieSyncStatus = "ok" SyncerCookieSyncPrivacyBlocked SyncerCookieSyncStatus = "privacy_blocked" SyncerCookieSyncAlreadySynced SyncerCookieSyncStatus = "already_synced" - SyncerCookieSyncTypeNotSupported SyncerCookieSyncStatus = "type_not_supported" + SyncerCookieSyncRejectedByFilter SyncerCookieSyncStatus = "rejected_by_filter" ) // SyncerRequestStatuses returns possible syncer statuses. @@ -370,7 +370,7 @@ func SyncerRequestStatuses() []SyncerCookieSyncStatus { SyncerCookieSyncOK, SyncerCookieSyncPrivacyBlocked, SyncerCookieSyncAlreadySynced, - SyncerCookieSyncTypeNotSupported, + SyncerCookieSyncRejectedByFilter, } } diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go index a74c8b6c0fa..15f8c3c23a5 100644 --- a/metrics/prometheus/prometheus_test.go +++ b/metrics/prometheus/prometheus_test.go @@ -141,7 +141,7 @@ func TestConnectionMetrics(t *testing.T) { func TestRequestMetric(t *testing.T) { m := createMetricsForTesting() requestType := metrics.ReqTypeORTB2Web - requestStatus := metrics.RequestStatusBlacklisted + requestStatus := metrics.RequestStatusBlockedApp m.RecordRequest(metrics.Labels{ RType: requestType, @@ -285,7 +285,7 @@ func TestRequestMetricWithoutCookie(t *testing.T) { performTest := func(m *Metrics, cookieFlag metrics.CookieFlag) { m.RecordRequest(metrics.Labels{ RType: requestType, - RequestStatus: metrics.RequestStatusBlacklisted, + RequestStatus: metrics.RequestStatusBlockedApp, CookieFlag: cookieFlag, }) } @@ -337,7 +337,7 @@ func TestAccountMetric(t *testing.T) { performTest := func(m *Metrics, pubID string) { m.RecordRequest(metrics.Labels{ RType: metrics.ReqTypeORTB2Web, - RequestStatus: metrics.RequestStatusBlacklisted, + RequestStatus: metrics.RequestStatusBlockedApp, PubID: pubID, }) } @@ -1235,7 +1235,7 @@ func TestRecordSyncerRequestMetric(t *testing.T) { label: "already_synced", }, { - status: metrics.SyncerCookieSyncTypeNotSupported, + status: metrics.SyncerCookieSyncRejectedByFilter, label: "type_not_supported", }, } diff --git a/modules/builder.go b/modules/builder.go index e5d04e149af..36ac5589add 100644 --- a/modules/builder.go +++ b/modules/builder.go @@ -1,6 +1,7 @@ package modules import ( + fiftyonedegreesDevicedetection "github.com/prebid/prebid-server/v2/modules/fiftyonedegrees/devicedetection" prebidOrtb2blocking "github.com/prebid/prebid-server/v2/modules/prebid/ortb2blocking" ) @@ -8,6 +9,9 @@ import ( // vendor and module names are chosen based on the module directory name func builders() ModuleBuilders { return ModuleBuilders{ + "fiftyonedegrees": { + "devicedetection": fiftyonedegreesDevicedetection.Builder, + }, "prebid": { "ortb2blocking": prebidOrtb2blocking.Builder, }, diff --git a/modules/fiftyonedegrees/devicedetection/README.md b/modules/fiftyonedegrees/devicedetection/README.md new file mode 100644 index 00000000000..645fb407fe5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/README.md @@ -0,0 +1,255 @@ +## Overview + +The 51Degrees module enriches an incoming OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/_device_detection__overview.html). + +The module sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which can be rapidly looked up in on premise data exposing over 250 properties including the device age, chip set, codec support, and price, operating system and app/browser versions, age, and embedded features. + +## Operation Details + +### Evidence + +The module uses `device.ua` (User Agent) and `device.sua` (Structured User Agent) provided in the oRTB request payload as input (or 'evidence' in 51Degrees terminology). There is a fallback to the corresponding HTTP request headers if any of these are not present in the oRTB payload - in particular: `User-Agent` and `Sec-CH-UA-*` (aka User-Agent Client Hints). To make sure Prebid.js sends Structured User Agent in the oRTB payload - we strongly advice publishers to enable [First Party Data Enrichment module](dev-docs/modules/enrichmentFpdModule.html) for their wrappers and specify + +```js +pbjs.setConfig({ + firstPartyData: { + uaHints: [ + 'architecture', + 'model', + 'platform', + 'platformVersion', + 'fullVersionList', + ] + } +}) +``` + +### Data File Updates + +The module operates **fully autonomously and does not make any requests to any cloud services in real time to do device detection**. This is an [on-premise data](https://51degrees.com/developers/deployment-options/on-premise-data) deployment in 51Degrees terminology. The module operates using a local data file that is loaded into memory fully or partially during operation. The data file is occasionally updated to accomodate new devices, so it is recommended to enable automatic data updates in the module configuration. Alternatively `watch_file_system` option can be used and the file may be downloaded and replaced on disk manually. See the configuration options below. + +## Setup + +The 51Degrees module operates using a data file. You can get started with a free Lite data file that can be downloaded here: [51Degrees-LiteV4.1.hash](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash). The Lite file is capable of detecting limited device information, so if you need in-depth device data, please contact 51Degrees to obtain a license: [https://51degrees.com/contact-us](https://51degrees.com/contact-us?ContactReason=Free%20Trial). + +Put the data file in a file system location writable by the system account that is running the Prebid Server module and specify that directory location in the configuration parameters. The location needs to be writable if you would like to enable [automatic data file updates](https://51degrees.com/documentation/_features__automatic_datafile_updates.html). + +### Execution Plan + +This module supports running at two stages: + +* entrypoint: this is where incoming requests are parsed and device detection evidences are extracted. +* raw-auction-request: this is where outgoing auction requests to each bidder are enriched with the device detection data + +We recommend defining the execution plan right in the account config +so the module is only invoked for specific accounts. See below for an example. + +### Global Config + +There is no host-company level config for this module. + +### Account-Level Config + +To start using current module in PBS-Go you have to enable module and add `fiftyone-devicedetection-entrypoint-hook` and `fiftyone-devicedetection-raw-auction-request-hook` into hooks execution plan inside your config file: +Here's a general template for the account config used in PBS-Go: + +```json +{ + "hooks": { + "enabled":true, + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "make_temp_copy": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "", + "polling_interval": 1800, + "license_key": "", + "product": "V4Enterprise", + "watch_file_system": "true", + "on_startup": true + } + } + } + }, + "host_execution_plan": { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-raw-auction-request-hook" + } + ] + } + ] + } + } + } + } + } + } + } +} +``` + +The same config in YAML format: +```yaml +hooks: + enabled: true + modules: + fiftyonedegrees: + devicedetection: + enabled: true + make_temp_copy: true + data_file: + path: path/to/51Degrees-LiteV4.1.hash + update: + auto: true + url: "" + polling_interval: 1800 + license_key: "" + product: V4Enterprise + watch_file_system: 'true' + host_execution_plan: + endpoints: + "/openrtb2/auction": + stages: + entrypoint: + groups: + - timeout: 10 + hook_sequence: + - module_code: fiftyonedegrees.devicedetection + hook_impl_code: fiftyone-devicedetection-entrypoint-hook + raw_auction_request: + groups: + - timeout: 10 + hook_sequence: + - module_code: fiftyonedegrees.devicedetection + hook_impl_code: fiftyone-devicedetection-raw-auction-request-hook +``` + +Note that at a minimum (besides adding to the host_execution_plan) you need to enable the module and specify a path to the data file in the configuration. +Sample module enablement configuration in JSON and YAML formats: + +```json +{ + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash" + } + } + } + } +} +``` + +```yaml + modules: + fiftyonedegrees: + devicedetection: + enabled: true + data_file: + path: "/path/to/51Degrees-LiteV4.1.hash" +``` + +## Module Configuration Parameters + +The parameter names are specified with full path using dot-notation. F.e. `section_name` .`sub_section` .`param_name` would result in this nesting in the JSON configuration: + +```json +{ + "section_name": { + "sub_section": { + "param_name": "param-value" + } + } +} +``` + +| Param Name | Required| Type | Default value | Description | +|:-------|:------|:------|:------|:---------------------------------------| +| `account_filter` .`allow_list` | No | list of strings | [] (empty list) | A list of account IDs that are allowed to use this module - only relevant if enabled globally for the host. If empty, all accounts are allowed. Full-string match is performed (whitespaces and capitalization matter). | +| `data_file` .`path` | **Yes** | string | null |The full path to the device detection data file. Sample file can be downloaded from [data repo on GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash), or get an Enterprise data file [here](https://51degrees.com/pricing). | +| `data_file` .`make_temp_copy` | No | boolean | true | If true, the engine will create a temporary copy of the data file rather than using the data file directly. | +| `data_file` .`update` .`auto` | No | boolean | true | If enabled, the engine will periodically (at predefined time intervals - see `polling-interval` parameter) check if new data file is available. When the new data file is available engine downloads it and switches to it for device detection. If custom `url` is not specified `license_key` param is required. | +| `data_file` .`update` .`on_startup` | No | boolean | false | If enabled, engine will check for the updated data file right away without waiting for the defined time interval. | +| `data_file` .`update` .`url` | No | string | null | Configure the engine to check the specified URL for the availability of the updated data file. If not specified the [51Degrees distributor service](https://51degrees.com/documentation/4.4/_info__distributor.html) URL will be used, which requires a License Key. | +| `data_file` .`update` .`license_key` | No | string | null | Required if `auto` is true and custom `url` is not specified. Allows to download the data file from the [51Degrees distributor service](https://51degrees.com/documentation/4.4/_info__distributor.html). | +| `data_file` .`update` .`watch_file_system` | No | boolean | true | If enabled the engine will watch the data file path for any changes, and automatically reload the data file from disk once it is updated. | +| `data_file` .`update` .`polling_interval` | No | int | 1800 | The time interval in seconds between consequent attempts to download an updated data file. Default = 1800 seconds = 30 minutes. | +| `data_file` .`update` .`product`| No | string | `V4Enterprise` | Set the Product used when checking for new device detection data files. A Product is exclusive to the 51Degrees paid service. Please see options [here](https://51degrees.com/documentation/_info__distributor.html). | +| `performance` .`profile` | No | string | `Balanced` | `performance.*` parameters are related to the tradeoffs between speed of device detection and RAM consumption or accuracy. `profile` dictates the proportion between the use of the RAM (the more RAM used - the faster is the device detection) and reads from disk (less RAM but slower device detection). Must be one of: `LowMemory`, `MaxPerformance`, `HighPerformance`, `Balanced`, `BalancedTemp`, `InMemory`. Defaults to `Balanced`. | +| `performance` .`concurrency` | No | int | 10 | Specify the expected number of concurrent operations that engine does. This sets the concurrency of the internal caches to avoid excessive locking. Default: 10. | +| `performance` .`difference` | No | int | 0 | Set the maximum difference to allow when processing evidence (HTTP headers). The meaning is the difference in hash value between the hash that was found, and the hash that is being searched for. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html). | +| `performance` .`drift` | No | int | 0 | Set the maximum drift to allow when matching hashes. If the drift is exceeded, the result is considered invalid and values will not be returned. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html). | +| `performance` .`allow_unmatched` | No | boolean | false | If set to false, a non-matching evidence will result in properties with no values set. If set to true, a non-matching evidence will cause the 'default profiles' to be returned. This means that properties will always have values (i.e. no need to check .hasValue) but some may be inaccurate. By default, this is false. | + +## Running the demo + +1. Download dependencies: +```bash +go mod download +``` + +2. Replace the original config file `pbs.json` (placed in the repository root or in `/etc/config`) with the sample [config file](sample/pbs.json): +``` +cp modules/fiftyonedegrees/devicedetection/sample/pbs.json pbs.json +``` + +3. Download `51Degrees-LiteV4.1.hash` from [[GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash)] and put it in the project root directory. + +```bash +curl -o 51Degrees-LiteV4.1.hash -L https://github.com/51Degrees/device-detection-data/raw/main/51Degrees-LiteV4.1.hash +``` + +4. Create a directory for sample stored requests (needed for the server to run): +```bash +mkdir -p sample/stored +``` + +5. Start the server: +```bash +go run main.go +``` + +6. Run sample request: +```bash +curl \ +--header "Content-Type: application/json" \ +http://localhost:8000/openrtb2/auction \ +--data @modules/fiftyonedegrees/devicedetection/sample/request_data.json +``` + +7. Observe the `device` object get enriched with `devicetype`, `os`, `osv`, `w`, `h` and `ext.fiftyonedegrees_deviceId`. + +## Maintainer contacts + +Any suggestions or questions can be directed to [support@51degrees.com](support@51degrees.com) e-mail. + +Or just open new [issue](https://github.com/prebid/prebid-server/issues/new) or [pull request](https://github.com/prebid/prebid-server/pulls) in this repository. diff --git a/modules/fiftyonedegrees/devicedetection/account_info_extractor.go b/modules/fiftyonedegrees/devicedetection/account_info_extractor.go new file mode 100644 index 00000000000..2a5168cfe0c --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_info_extractor.go @@ -0,0 +1,37 @@ +package devicedetection + +import ( + "github.com/tidwall/gjson" +) + +type accountInfo struct { + Id string +} + +type accountInfoExtractor struct{} + +func newAccountInfoExtractor() accountInfoExtractor { + return accountInfoExtractor{} +} + +// extract extracts the account information from the payload +// The account information is extracted from the publisher id or site publisher id +func (x accountInfoExtractor) extract(payload []byte) *accountInfo { + if payload == nil { + return nil + } + + publisherResult := gjson.GetBytes(payload, "app.publisher.id") + if publisherResult.Exists() { + return &accountInfo{ + Id: publisherResult.String(), + } + } + publisherResult = gjson.GetBytes(payload, "site.publisher.id") + if publisherResult.Exists() { + return &accountInfo{ + Id: publisherResult.String(), + } + } + return nil +} diff --git a/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go b/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go new file mode 100644 index 00000000000..2d32f7915b5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go @@ -0,0 +1,74 @@ +package devicedetection + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + siteRequestPayload = []byte(` + { + "site": { + "publisher": { + "id": "p-bid-config-test-005" + } + } + } + `) + + mobileRequestPayload = []byte(` + { + "app": { + "publisher": { + "id": "p-bid-config-test-005" + } + } + } + `) + + emptyPayload = []byte(`{}`) +) + +func TestPublisherIdExtraction(t *testing.T) { + tests := []struct { + name string + payload []byte + expected string + expectNil bool + }{ + { + name: "SiteRequest", + payload: siteRequestPayload, + expected: "p-bid-config-test-005", + }, + { + name: "MobileRequest", + payload: mobileRequestPayload, + expected: "p-bid-config-test-005", + }, + { + name: "EmptyPublisherId", + payload: emptyPayload, + expectNil: true, + }, + { + name: "EmptyPayload", + payload: nil, + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractor := newAccountInfoExtractor() + accountInfo := extractor.extract(tt.payload) + + if tt.expectNil { + assert.Nil(t, accountInfo) + } else { + assert.Equal(t, tt.expected, accountInfo.Id) + } + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/account_validator.go b/modules/fiftyonedegrees/devicedetection/account_validator.go new file mode 100644 index 00000000000..fdff92531a7 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_validator.go @@ -0,0 +1,28 @@ +package devicedetection + +import "slices" + +// defaultAccountValidator is a struct that contains an accountInfoExtractor +// and is used to validate if an account is allowed +type defaultAccountValidator struct { + AccountExtractor accountInfoExtractor +} + +func newAccountValidator() *defaultAccountValidator { + return &defaultAccountValidator{ + AccountExtractor: newAccountInfoExtractor(), + } +} + +func (x defaultAccountValidator) isAllowed(cfg config, req []byte) bool { + if len(cfg.AccountFilter.AllowList) == 0 { + return true + } + + accountInfo := x.AccountExtractor.extract(req) + if accountInfo != nil && slices.Contains(cfg.AccountFilter.AllowList, accountInfo.Id) { + return true + } + + return false +} diff --git a/modules/fiftyonedegrees/devicedetection/account_validator_test.go b/modules/fiftyonedegrees/devicedetection/account_validator_test.go new file mode 100644 index 00000000000..25f99e3b796 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_validator_test.go @@ -0,0 +1,71 @@ +package devicedetection + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + + "github.com/stretchr/testify/assert" +) + +func TestIsAllowed(t *testing.T) { + tests := []struct { + name string + allowList []string + expectedResult bool + }{ + { + name: "allowed", + allowList: []string{"1001"}, + expectedResult: true, + }, + { + name: "empty", + allowList: []string{}, + expectedResult: true, + }, + { + name: "disallowed", + allowList: []string{"1002"}, + expectedResult: false, + }, + { + name: "allow_list_is_nil", + allowList: nil, + expectedResult: true, + }, + { + name: "allow_list_contains_multiple", + allowList: []string{"1000", "1001", "1002"}, + expectedResult: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validator := newAccountValidator() + cfg := config{ + AccountFilter: accountFilter{AllowList: test.allowList}, + } + + res := validator.isAllowed( + cfg, toBytes( + &openrtb2.BidRequest{ + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{ + ID: "1001", + }, + }, + }, + ), + ) + assert.Equal(t, test.expectedResult, res) + }) + } +} + +func toBytes(v interface{}) []byte { + res, _ := json.Marshal(v) + return res +} diff --git a/modules/fiftyonedegrees/devicedetection/config.go b/modules/fiftyonedegrees/devicedetection/config.go new file mode 100644 index 00000000000..a5519026791 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/config.go @@ -0,0 +1,80 @@ +package devicedetection + +import ( + "encoding/json" + "os" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/pkg/errors" + + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type config struct { + DataFile dataFile `json:"data_file"` + AccountFilter accountFilter `json:"account_filter"` + Performance performance `json:"performance"` +} + +type dataFile struct { + Path string `json:"path"` + Update dataFileUpdate `json:"update"` + MakeTempCopy *bool `json:"make_temp_copy"` +} + +type dataFileUpdate struct { + Auto bool `json:"auto"` + Url string `json:"url"` + License string `json:"license_key"` + PollingInterval int `json:"polling_interval"` + Product string `json:"product"` + WatchFileSystem *bool `json:"watch_file_system"` + OnStartup bool `json:"on_startup"` +} + +type accountFilter struct { + AllowList []string `json:"allow_list"` +} + +type performance struct { + Profile string `json:"profile"` + Concurrency *int `json:"concurrency"` + Difference *int `json:"difference"` + AllowUnmatched *bool `json:"allow_unmatched"` + Drift *int `json:"drift"` +} + +var performanceProfileMap = map[string]dd.PerformanceProfile{ + "Default": dd.Default, + "LowMemory": dd.LowMemory, + "BalancedTemp": dd.BalancedTemp, + "Balanced": dd.Balanced, + "HighPerformance": dd.HighPerformance, + "InMemory": dd.InMemory, +} + +func (c *config) getPerformanceProfile() dd.PerformanceProfile { + mappedResult, ok := performanceProfileMap[c.Performance.Profile] + if !ok { + return dd.Default + } + + return mappedResult +} + +func parseConfig(data json.RawMessage) (config, error) { + var cfg config + if err := jsonutil.UnmarshalValid(data, &cfg); err != nil { + return cfg, errors.Wrap(err, "failed to parse config") + } + return cfg, nil +} + +func validateConfig(cfg config) error { + _, err := os.Stat(cfg.DataFile.Path) + if err != nil { + return errors.Wrap(err, "error opening hash file path") + } + + return nil +} diff --git a/modules/fiftyonedegrees/devicedetection/config_test.go b/modules/fiftyonedegrees/devicedetection/config_test.go new file mode 100644 index 00000000000..e2478d82b7d --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/config_test.go @@ -0,0 +1,119 @@ +package devicedetection + +import ( + "os" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/stretchr/testify/assert" +) + +func TestParseConfig(t *testing.T) { + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "license_key": "your_license_key", + "product": "V4Enterprise", + "on_startup": true + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + + cfg, err := parseConfig(cfgRaw) + + assert.NoError(t, err) + + assert.Equal(t, cfg.DataFile.Path, "path/to/51Degrees-LiteV4.1.hash") + assert.True(t, cfg.DataFile.Update.Auto) + assert.Equal(t, cfg.DataFile.Update.Url, "https://my.datafile.com/datafile.gz") + assert.Equal(t, cfg.DataFile.Update.PollingInterval, 3600) + assert.Equal(t, cfg.DataFile.Update.License, "your_license_key") + assert.Equal(t, cfg.DataFile.Update.Product, "V4Enterprise") + assert.True(t, cfg.DataFile.Update.OnStartup) + assert.Equal(t, cfg.AccountFilter.AllowList, []string{"123"}) + assert.Equal(t, cfg.Performance.Profile, "default") + assert.Equal(t, *cfg.Performance.Concurrency, 1) + assert.Equal(t, *cfg.Performance.Difference, 1) + assert.True(t, *cfg.Performance.AllowUnmatched) + assert.Equal(t, *cfg.Performance.Drift, 1) + assert.Equal(t, cfg.getPerformanceProfile(), dd.Default) +} + +func TestValidateConfig(t *testing.T) { + file, err := os.Create("test-validate-config.hash") + if err != nil { + t.Errorf("Failed to create file: %v", err) + } + defer file.Close() + defer os.Remove("test-validate-config.hash") + + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "test-validate-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + + cfg, err := parseConfig(cfgRaw) + assert.NoError(t, err) + + err = validateConfig(cfg) + assert.NoError(t, err) + +} + +func TestInvalidPerformanceProfile(t *testing.T) { + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "test-validate-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "123", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + cfg, err := parseConfig(cfgRaw) + assert.NoError(t, err) + + assert.Equal(t, cfg.getPerformanceProfile(), dd.Default) +} diff --git a/modules/fiftyonedegrees/devicedetection/context.go b/modules/fiftyonedegrees/devicedetection/context.go new file mode 100644 index 00000000000..3c10dd2f393 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/context.go @@ -0,0 +1,8 @@ +package devicedetection + +// Context keys for device detection +const ( + evidenceFromHeadersCtxKey = "evidence_from_headers" + evidenceFromSuaCtxKey = "evidence_from_sua" + ddEnabledCtxKey = "dd_enabled" +) diff --git a/modules/fiftyonedegrees/devicedetection/device_detector.go b/modules/fiftyonedegrees/devicedetection/device_detector.go new file mode 100644 index 00000000000..8369d343d34 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_detector.go @@ -0,0 +1,157 @@ +package devicedetection + +import ( + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" +) + +type engine interface { + Process(evidences []onpremise.Evidence) (*dd.ResultsHash, error) + GetHttpHeaderKeys() []dd.EvidenceKey +} + +type extractor interface { + extract(results Results, ua string) (*deviceInfo, error) +} + +type defaultDeviceDetector struct { + cfg *dd.ConfigHash + deviceInfoExtractor extractor + engine engine +} + +func newDeviceDetector(cfg *dd.ConfigHash, moduleConfig *config) (*defaultDeviceDetector, error) { + engineOptions := buildEngineOptions(moduleConfig, cfg) + + ddEngine, err := onpremise.New( + engineOptions..., + ) + if err != nil { + return nil, errors.Wrap(err, "Failed to create onpremise engine.") + } + + deviceDetector := &defaultDeviceDetector{ + engine: ddEngine, + cfg: cfg, + deviceInfoExtractor: newDeviceInfoExtractor(), + } + + return deviceDetector, nil +} + +func buildEngineOptions(moduleConfig *config, configHash *dd.ConfigHash) []onpremise.EngineOptions { + options := []onpremise.EngineOptions{ + onpremise.WithDataFile(moduleConfig.DataFile.Path), + } + + options = append( + options, + onpremise.WithProperties([]string{ + "HardwareVendor", + "HardwareName", + "DeviceType", + "PlatformVendor", + "PlatformName", + "PlatformVersion", + "BrowserVendor", + "BrowserName", + "BrowserVersion", + "ScreenPixelsWidth", + "ScreenPixelsHeight", + "PixelRatio", + "Javascript", + "GeoLocation", + "HardwareModel", + "HardwareFamily", + "HardwareModelVariants", + "ScreenInchesHeight", + "IsCrawler", + }), + ) + + options = append( + options, + onpremise.WithConfigHash(configHash), + ) + + if moduleConfig.DataFile.MakeTempCopy != nil { + options = append( + options, + onpremise.WithTempDataCopy(*moduleConfig.DataFile.MakeTempCopy), + ) + } + + dataUpdateOptions := []onpremise.EngineOptions{ + onpremise.WithAutoUpdate(moduleConfig.DataFile.Update.Auto), + } + + if moduleConfig.DataFile.Update.Url != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithDataUpdateUrl( + moduleConfig.DataFile.Update.Url, + ), + ) + } + + if moduleConfig.DataFile.Update.PollingInterval > 0 { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithPollingInterval( + moduleConfig.DataFile.Update.PollingInterval, + ), + ) + } + + if moduleConfig.DataFile.Update.License != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithLicenseKey(moduleConfig.DataFile.Update.License), + ) + } + + if moduleConfig.DataFile.Update.Product != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithProduct(moduleConfig.DataFile.Update.Product), + ) + } + + if moduleConfig.DataFile.Update.WatchFileSystem != nil { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithFileWatch( + *moduleConfig.DataFile.Update.WatchFileSystem, + ), + ) + } + + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithUpdateOnStart(moduleConfig.DataFile.Update.OnStartup), + ) + + options = append( + options, + dataUpdateOptions..., + ) + + return options +} + +func (x defaultDeviceDetector) getSupportedHeaders() []dd.EvidenceKey { + return x.engine.GetHttpHeaderKeys() +} + +func (x defaultDeviceDetector) getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) { + results, err := x.engine.Process(evidence) + if err != nil { + return nil, errors.Wrap(err, "Failed to process evidence") + } + defer results.Free() + + deviceInfo, err := x.deviceInfoExtractor.extract(results, ua) + + return deviceInfo, err +} diff --git a/modules/fiftyonedegrees/devicedetection/device_detector_test.go b/modules/fiftyonedegrees/devicedetection/device_detector_test.go new file mode 100644 index 00000000000..84d6ab28cc0 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_detector_test.go @@ -0,0 +1,190 @@ +package devicedetection + +import ( + "fmt" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestBuildEngineOptions(t *testing.T) { + cases := []struct { + cfgRaw []byte + length int + }{ + { + cfgRaw: []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "license_key": "your_license_key", + "product": "V4Enterprise", + "watch_file_system": true, + "on_startup": true + }, + "make_temp_copy": true + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), + length: 11, + // data_file.path, data_file.update.auto:true, url, polling_interval, license_key, product, confighash, properties + // data_file.update.on_startup:true, data_file.update.watch_file_system:true, data_file.make_temp_copy:true + }, + { + cfgRaw: []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash" + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), + length: 5, // data_file.update.auto:false, data_file.path, confighash, properties, data_file.update.on_startup:false + }, + } + + for _, c := range cases { + cfg, err := parseConfig(c.cfgRaw) + assert.NoError(t, err) + configHash := configHashFromConfig(&cfg) + options := buildEngineOptions(&cfg, configHash) + assert.Equal(t, c.length, len(options)) + } +} + +type engineMock struct { + mock.Mock +} + +func (e *engineMock) Process(evidences []onpremise.Evidence) (*dd.ResultsHash, error) { + args := e.Called(evidences) + res := args.Get(0) + if res == nil { + return nil, args.Error(1) + } + + return res.(*dd.ResultsHash), args.Error(1) +} + +func (e *engineMock) GetHttpHeaderKeys() []dd.EvidenceKey { + args := e.Called() + return args.Get(0).([]dd.EvidenceKey) +} + +type extractorMock struct { + mock.Mock +} + +func (e *extractorMock) extract(results Results, ua string) (*deviceInfo, error) { + args := e.Called(results, ua) + return args.Get(0).(*deviceInfo), args.Error(1) +} + +func TestGetDeviceInfo(t *testing.T) { + tests := []struct { + name string + engineResponse *dd.ResultsHash + engineError error + expectedResult *deviceInfo + expectedError string + }{ + { + name: "Success_path", + engineResponse: &dd.ResultsHash{}, + engineError: nil, + expectedResult: &deviceInfo{ + DeviceId: "123", + }, + expectedError: "", + }, + { + name: "Error_path", + engineResponse: nil, + engineError: fmt.Errorf("error"), + expectedResult: nil, + expectedError: "Failed to process evidence: error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractorM := &extractorMock{} + extractorM.On("extract", mock.Anything, mock.Anything).Return( + &deviceInfo{ + DeviceId: "123", + }, nil, + ) + + engineM := &engineMock{} + engineM.On("Process", mock.Anything).Return( + tt.engineResponse, tt.engineError, + ) + + deviceDetector := defaultDeviceDetector{ + cfg: nil, + deviceInfoExtractor: extractorM, + engine: engineM, + } + + result, err := deviceDetector.getDeviceInfo( + []onpremise.Evidence{{ + Prefix: dd.HttpEvidenceQuery, + Key: "key", + Value: "val", + }}, "ua", + ) + + if tt.expectedError == "" { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.expectedResult.DeviceId, result.DeviceId) + } else { + assert.Errorf(t, err, tt.expectedError) + assert.Nil(t, result) + } + }) + } +} + +func TestGetSupportedHeaders(t *testing.T) { + engineM := &engineMock{} + + engineM.On("GetHttpHeaderKeys").Return( + []dd.EvidenceKey{{ + Key: "key", + Prefix: dd.HttpEvidenceQuery, + }}, + ) + + deviceDetector := defaultDeviceDetector{ + cfg: nil, + deviceInfoExtractor: nil, + engine: engineM, + } + + result := deviceDetector.getSupportedHeaders() + assert.NotNil(t, result) + assert.Equal(t, len(result), 1) + assert.Equal(t, result[0].Key, "key") + +} diff --git a/modules/fiftyonedegrees/devicedetection/device_info_extractor.go b/modules/fiftyonedegrees/devicedetection/device_info_extractor.go new file mode 100644 index 00000000000..1c913e21696 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_info_extractor.go @@ -0,0 +1,121 @@ +package devicedetection + +import ( + "strconv" + + "github.com/golang/glog" + "github.com/pkg/errors" +) + +// deviceInfoExtractor is a struct that contains the methods to extract device information +// from the results of the device detection +type deviceInfoExtractor struct{} + +func newDeviceInfoExtractor() deviceInfoExtractor { + return deviceInfoExtractor{} +} + +type Results interface { + ValuesString(string, string) (string, error) + HasValues(string) (bool, error) + DeviceId() (string, error) +} + +type deviceInfoProperty string + +const ( + deviceInfoHardwareVendor deviceInfoProperty = "HardwareVendor" + deviceInfoHardwareName deviceInfoProperty = "HardwareName" + deviceInfoDeviceType deviceInfoProperty = "DeviceType" + deviceInfoPlatformVendor deviceInfoProperty = "PlatformVendor" + deviceInfoPlatformName deviceInfoProperty = "PlatformName" + deviceInfoPlatformVersion deviceInfoProperty = "PlatformVersion" + deviceInfoBrowserVendor deviceInfoProperty = "BrowserVendor" + deviceInfoBrowserName deviceInfoProperty = "BrowserName" + deviceInfoBrowserVersion deviceInfoProperty = "BrowserVersion" + deviceInfoScreenPixelsWidth deviceInfoProperty = "ScreenPixelsWidth" + deviceInfoScreenPixelsHeight deviceInfoProperty = "ScreenPixelsHeight" + deviceInfoPixelRatio deviceInfoProperty = "PixelRatio" + deviceInfoJavascript deviceInfoProperty = "Javascript" + deviceInfoGeoLocation deviceInfoProperty = "GeoLocation" + deviceInfoHardwareModel deviceInfoProperty = "HardwareModel" + deviceInfoHardwareFamily deviceInfoProperty = "HardwareFamily" + deviceInfoHardwareModelVariants deviceInfoProperty = "HardwareModelVariants" + deviceInfoScreenInchesHeight deviceInfoProperty = "ScreenInchesHeight" +) + +func (x deviceInfoExtractor) extract(results Results, ua string) (*deviceInfo, error) { + hardwareVendor := x.getValue(results, deviceInfoHardwareVendor) + hardwareName := x.getValue(results, deviceInfoHardwareName) + deviceType := x.getValue(results, deviceInfoDeviceType) + platformVendor := x.getValue(results, deviceInfoPlatformVendor) + platformName := x.getValue(results, deviceInfoPlatformName) + platformVersion := x.getValue(results, deviceInfoPlatformVersion) + browserVendor := x.getValue(results, deviceInfoBrowserVendor) + browserName := x.getValue(results, deviceInfoBrowserName) + browserVersion := x.getValue(results, deviceInfoBrowserVersion) + screenPixelsWidth, _ := strconv.ParseInt(x.getValue(results, deviceInfoScreenPixelsWidth), 10, 64) + screenPixelsHeight, _ := strconv.ParseInt(x.getValue(results, deviceInfoScreenPixelsHeight), 10, 64) + pixelRatio, _ := strconv.ParseFloat(x.getValue(results, deviceInfoPixelRatio), 10) + javascript, _ := strconv.ParseBool(x.getValue(results, deviceInfoJavascript)) + geoLocation, _ := strconv.ParseBool(x.getValue(results, deviceInfoGeoLocation)) + deviceId, err := results.DeviceId() + if err != nil { + return nil, errors.Wrap(err, "Failed to get device id.") + } + hardwareModel := x.getValue(results, deviceInfoHardwareModel) + hardwareFamily := x.getValue(results, deviceInfoHardwareFamily) + hardwareModelVariants := x.getValue(results, deviceInfoHardwareModelVariants) + screenInchedHeight, _ := strconv.ParseFloat(x.getValue(results, deviceInfoScreenInchesHeight), 10) + + p := &deviceInfo{ + HardwareVendor: hardwareVendor, + HardwareName: hardwareName, + DeviceType: deviceType, + PlatformVendor: platformVendor, + PlatformName: platformName, + PlatformVersion: platformVersion, + BrowserVendor: browserVendor, + BrowserName: browserName, + BrowserVersion: browserVersion, + ScreenPixelsWidth: screenPixelsWidth, + ScreenPixelsHeight: screenPixelsHeight, + PixelRatio: pixelRatio, + Javascript: javascript, + GeoLocation: geoLocation, + UserAgent: ua, + DeviceId: deviceId, + HardwareModel: hardwareModel, + HardwareFamily: hardwareFamily, + HardwareModelVariants: hardwareModelVariants, + ScreenInchesHeight: screenInchedHeight, + } + + return p, nil +} + +// function getValue return a value results for a property +func (x deviceInfoExtractor) getValue(results Results, propertyName deviceInfoProperty) string { + // Get the values in string + value, err := results.ValuesString( + string(propertyName), + ",", + ) + if err != nil { + glog.Errorf("Failed to get results values string.") + return "" + } + + hasValues, err := results.HasValues(string(propertyName)) + if err != nil { + glog.Errorf("Failed to check if a matched value exists for property %s.\n", propertyName) + return "" + } + + if !hasValues { + glog.Warningf("Property %s does not have a matched value.\n", propertyName) + return "Unknown" + } + + return value +} diff --git a/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go b/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go new file mode 100644 index 00000000000..197e3928602 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go @@ -0,0 +1,130 @@ +package devicedetection + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type ResultsHashMock struct { + mock.Mock +} + +func (m *ResultsHashMock) DeviceId() (string, error) { + return "", nil +} + +func (m *ResultsHashMock) ValuesString(prop1 string, prop2 string) (string, error) { + args := m.Called(prop1, prop2) + return args.String(0), args.Error(1) +} + +func (m *ResultsHashMock) HasValues(prop1 string) (bool, error) { + args := m.Called(prop1) + return args.Bool(0), args.Error(1) +} + +func TestDeviceInfoExtraction(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValue(results, "HardwareName", "Macbook") + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + + assert.Equal(t, deviceInfo.HardwareName, "Macbook") + assertDeviceInfo(t, deviceInfo) +} + +func TestDeviceInfoExtractionNoProperty(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + results.Mock.On("ValuesString", "HardwareName", ",").Return("", errors.New("Error")) + results.Mock.On("HasValues", "HardwareName").Return(true, nil) + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "") +} + +func TestDeviceInfoExtractionNoValue(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValues(results) + mockValue(results, "HardwareVendor", "Apple") + + results.Mock.On("ValuesString", "HardwareName", ",").Return("Macbook", nil) + results.Mock.On("HasValues", "HardwareName").Return(false, nil) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "Unknown") +} + +func TestDeviceInfoExtractionHasValueError(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValue(results, "HardwareVendor", "Apple") + + results.Mock.On("ValuesString", "HardwareName", ",").Return("Macbook", nil) + results.Mock.On("HasValues", "HardwareName").Return(true, errors.New("error")) + + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "") +} + +func mockValues(results *ResultsHashMock) { + mockValue(results, "HardwareVendor", "Apple") + mockValue(results, "DeviceType", "Desctop") + mockValue(results, "PlatformVendor", "Apple") + mockValue(results, "PlatformName", "MacOs") + mockValue(results, "PlatformVersion", "14") + mockValue(results, "BrowserVendor", "Google") + mockValue(results, "BrowserName", "Crome") + mockValue(results, "BrowserVersion", "12") + mockValue(results, "ScreenPixelsWidth", "1024") + mockValue(results, "ScreenPixelsHeight", "1080") + mockValue(results, "PixelRatio", "223") + mockValue(results, "Javascript", "true") + mockValue(results, "GeoLocation", "true") + mockValue(results, "HardwareModel", "Macbook") + mockValue(results, "HardwareFamily", "Macbook") + mockValue(results, "HardwareModelVariants", "Macbook") + mockValue(results, "ScreenInchesHeight", "12") +} + +func assertDeviceInfo(t *testing.T, deviceInfo *deviceInfo) { + assert.Equal(t, deviceInfo.HardwareVendor, "Apple") + assert.Equal(t, deviceInfo.DeviceType, "Desctop") + assert.Equal(t, deviceInfo.PlatformVendor, "Apple") + assert.Equal(t, deviceInfo.PlatformName, "MacOs") + assert.Equal(t, deviceInfo.PlatformVersion, "14") + assert.Equal(t, deviceInfo.BrowserVendor, "Google") + assert.Equal(t, deviceInfo.BrowserName, "Crome") + assert.Equal(t, deviceInfo.BrowserVersion, "12") + assert.Equal(t, deviceInfo.ScreenPixelsWidth, int64(1024)) + assert.Equal(t, deviceInfo.ScreenPixelsHeight, int64(1080)) + assert.Equal(t, deviceInfo.PixelRatio, float64(223)) + assert.Equal(t, deviceInfo.Javascript, true) + assert.Equal(t, deviceInfo.GeoLocation, true) +} + +func mockValue(results *ResultsHashMock, name string, value string) { + results.Mock.On("ValuesString", name, ",").Return(value, nil) + results.Mock.On("HasValues", name).Return(true, nil) +} diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go new file mode 100644 index 00000000000..1d67e1cdeed --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go @@ -0,0 +1,118 @@ +package devicedetection + +import ( + "net/http" + + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/prebid/prebid-server/v2/hooks/hookstage" +) + +type defaultEvidenceExtractor struct { + valFromHeaders evidenceFromRequestHeadersExtractor + valFromSUA evidenceFromSUAPayloadExtractor +} + +func newEvidenceExtractor() *defaultEvidenceExtractor { + evidenceExtractor := &defaultEvidenceExtractor{ + valFromHeaders: newEvidenceFromRequestHeadersExtractor(), + valFromSUA: newEvidenceFromSUAPayloadExtractor(), + } + + return evidenceExtractor +} + +func (x *defaultEvidenceExtractor) fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + return x.valFromHeaders.extract(request, httpHeaderKeys) +} + +func (x *defaultEvidenceExtractor) fromSuaPayload(payload []byte) []stringEvidence { + return x.valFromSUA.extract(payload) +} + +// merge merges two slices of stringEvidence into one slice of stringEvidence +func merge(val1, val2 []stringEvidence) []stringEvidence { + evidenceMap := make(map[string]stringEvidence) + for _, e := range val1 { + evidenceMap[e.Key] = e + } + + for _, e := range val2 { + _, exists := evidenceMap[e.Key] + if !exists { + evidenceMap[e.Key] = e + } + } + + evidence := make([]stringEvidence, 0) + + for _, e := range evidenceMap { + evidence = append(evidence, e) + } + + return evidence +} + +func (x *defaultEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { + if ctx == nil { + return nil, "", errors.New("context is nil") + } + + suaStrings, err := x.getEvidenceStrings(ctx[evidenceFromSuaCtxKey]) + if err != nil { + return nil, "", errors.Wrap(err, "error extracting sua evidence") + } + headerString, err := x.getEvidenceStrings(ctx[evidenceFromHeadersCtxKey]) + if err != nil { + return nil, "", errors.Wrap(err, "error extracting header evidence") + } + + // Merge evidence from headers and SUA, sua has higher priority + evidenceStrings := merge(suaStrings, headerString) + + if len(evidenceStrings) > 0 { + userAgentE, exists := getEvidenceByKey(evidenceStrings, userAgentHeader) + if !exists { + return nil, "", errors.New("User-Agent not found") + } + + evidence := x.extractEvidenceFromStrings(evidenceStrings) + + return evidence, userAgentE.Value, nil + } + + return nil, "", nil +} + +func (x *defaultEvidenceExtractor) getEvidenceStrings(source interface{}) ([]stringEvidence, error) { + if source == nil { + return []stringEvidence{}, nil + } + + evidenceStrings, ok := source.([]stringEvidence) + if !ok { + return nil, errors.New("bad cast to []stringEvidence") + } + + return evidenceStrings, nil +} + +func (x *defaultEvidenceExtractor) extractEvidenceFromStrings(strEvidence []stringEvidence) []onpremise.Evidence { + evidenceResult := make([]onpremise.Evidence, len(strEvidence)) + for i, e := range strEvidence { + prefix := dd.HttpHeaderString + if e.Prefix == queryPrefix { + prefix = dd.HttpEvidenceQuery + } + + evidenceResult[i] = onpremise.Evidence{ + Prefix: prefix, + Key: e.Key, + Value: e.Value, + } + } + + return evidenceResult +} diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go new file mode 100644 index 00000000000..9abdf799643 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go @@ -0,0 +1,256 @@ +package devicedetection + +import ( + "net/http" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/stretchr/testify/assert" +) + +func TestFromHeaders(t *testing.T) { + extractor := newEvidenceExtractor() + + req := http.Request{ + Header: make(map[string][]string), + } + req.Header.Add("header", "Value") + req.Header.Add("Sec-CH-UA-Full-Version-List", "Chrome;12") + evidenceKeys := []dd.EvidenceKey{ + { + Prefix: dd.EvidencePrefix(10), + Key: "header", + }, + { + Prefix: dd.EvidencePrefix(10), + Key: "Sec-CH-UA-Full-Version-List", + }, + } + + evidence := extractor.fromHeaders(&req, evidenceKeys) + + assert.NotNil(t, evidence) + assert.NotEmpty(t, evidence) + assert.Equal(t, evidence[0].Value, "Value") + assert.Equal(t, evidence[0].Key, "header") + assert.Equal(t, evidence[1].Value, "Chrome;12") + assert.Equal(t, evidence[1].Key, "Sec-CH-UA-Full-Version-List") +} + +func TestFromSuaPayload(t *testing.T) { + tests := []struct { + name string + payload []byte + evidenceSize int + evidenceKeyOrder int + expectedKey string + expectedValue string + }{ + { + name: "from_SUA_tag", + payload: []byte(`{ + "device": { + "sua": { + "browsers": [ + { + "brand": "Google Chrome", + "version": ["121", "0", "6167", "184"] + } + ], + "platform": { + "brand": "macOS", + "version": ["14", "0", "0"] + }, + "architecture": "arm" + } + } + }`), + evidenceSize: 4, + evidenceKeyOrder: 0, + expectedKey: "Sec-Ch-Ua-Arch", + expectedValue: "arm", + }, + { + name: "from_UA_headers", + payload: []byte(`{ + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "sua": { + "architecture": "arm" + } + } + }`), + evidenceSize: 2, + evidenceKeyOrder: 1, + expectedKey: "Sec-Ch-Ua-Arch", + expectedValue: "arm", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractor := newEvidenceExtractor() + + evidence := extractor.fromSuaPayload(tt.payload) + + assert.NotNil(t, evidence) + assert.NotEmpty(t, evidence) + assert.Equal(t, len(evidence), tt.evidenceSize) + assert.Equal(t, evidence[tt.evidenceKeyOrder].Key, tt.expectedKey) + assert.Equal(t, evidence[tt.evidenceKeyOrder].Value, tt.expectedValue) + }) + } +} + +func TestExtract(t *testing.T) { + uaEvidence1 := stringEvidence{ + Prefix: "ua1", + Key: userAgentHeader, + Value: "uav1", + } + uaEvidence2 := stringEvidence{ + Prefix: "ua2", + Key: userAgentHeader, + Value: "uav2", + } + evidence1 := stringEvidence{ + Prefix: "e1", + Key: "k1", + Value: "v1", + } + emptyEvidence := stringEvidence{ + Prefix: "empty", + Key: "e1", + Value: "", + } + + tests := []struct { + name string + ctx hookstage.ModuleContext + wantEvidenceCount int + wantUserAgent string + wantError bool + }{ + { + name: "nil", + ctx: nil, + wantError: true, + }, + { + name: "empty", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{}, + evidenceFromHeadersCtxKey: []stringEvidence{}, + }, + wantEvidenceCount: 0, + wantUserAgent: "", + }, + { + name: "from_headers", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav1", + }, + { + name: "from_headers_no_user_agent", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{evidence1}, + }, + wantError: true, + }, + { + name: "from_sua", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{uaEvidence1}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav1", + }, + { + name: "from_sua_no_user_agent", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{evidence1}, + }, + wantError: true, + }, + { + name: "from_headers_error", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: "bad value", + }, + wantError: true, + }, + { + name: "from_sua_error", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{}, + evidenceFromSuaCtxKey: "bad value", + }, + wantError: true, + }, + { + name: "from_sua_and_headers", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: []stringEvidence{evidence1}, + }, + wantEvidenceCount: 2, + wantUserAgent: "uav1", + }, + { + name: "from_sua_and_headers_sua_can_overwrite_if_ua_present", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: []stringEvidence{uaEvidence2}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav2", + }, + { + name: "empty_string_values", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{emptyEvidence}, + }, + wantError: true, + }, + { + name: "empty_sua_values", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{emptyEvidence}, + }, + wantError: true, + }, + { + name: "mixed_valid_and_invalid", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: "bad value", + }, + wantError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + extractor := newEvidenceExtractor() + evidence, userAgent, err := extractor.extract(test.ctx) + + if test.wantError { + assert.Error(t, err) + assert.Nil(t, evidence) + assert.Equal(t, userAgent, "") + } else if test.wantEvidenceCount == 0 { + assert.NoError(t, err) + assert.Nil(t, evidence) + assert.Equal(t, userAgent, "") + } else { + assert.NoError(t, err) + assert.Equal(t, len(evidence), test.wantEvidenceCount) + assert.Equal(t, userAgent, test.wantUserAgent) + } + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go new file mode 100644 index 00000000000..7237698117d --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go @@ -0,0 +1,77 @@ +package devicedetection + +import ( + "github.com/prebid/openrtb/v20/adcom1" +) + +type deviceTypeMap = map[deviceType]adcom1.DeviceType + +var mobileOrTabletDeviceTypes = []deviceType{ + deviceTypeMobile, + deviceTypeSmartPhone, +} + +var personalComputerDeviceTypes = []deviceType{ + deviceTypeDesktop, + deviceTypeEReader, + deviceTypeVehicleDisplay, +} + +var tvDeviceTypes = []deviceType{ + deviceTypeTv, +} + +var phoneDeviceTypes = []deviceType{ + deviceTypePhone, +} + +var tabletDeviceTypes = []deviceType{ + deviceTypeTablet, +} + +var connectedDeviceTypes = []deviceType{ + deviceTypeIoT, + deviceTypeRouter, + deviceTypeSmallScreen, + deviceTypeSmartSpeaker, + deviceTypeSmartWatch, +} + +var setTopBoxDeviceTypes = []deviceType{ + deviceTypeMediaHub, + deviceTypeConsole, +} + +var oohDeviceTypes = []deviceType{ + deviceTypeKiosk, +} + +func applyCollection(items []deviceType, value adcom1.DeviceType, mappedCollection deviceTypeMap) { + for _, item := range items { + mappedCollection[item] = value + } +} + +var deviceTypeMapCollection = deviceTypeMap{} + +func init() { + applyCollection(mobileOrTabletDeviceTypes, adcom1.DeviceMobile, deviceTypeMapCollection) + applyCollection(personalComputerDeviceTypes, adcom1.DevicePC, deviceTypeMapCollection) + applyCollection(tvDeviceTypes, adcom1.DeviceTV, deviceTypeMapCollection) + applyCollection(phoneDeviceTypes, adcom1.DevicePhone, deviceTypeMapCollection) + applyCollection(tabletDeviceTypes, adcom1.DeviceTablet, deviceTypeMapCollection) + applyCollection(connectedDeviceTypes, adcom1.DeviceConnected, deviceTypeMapCollection) + applyCollection(setTopBoxDeviceTypes, adcom1.DeviceSetTopBox, deviceTypeMapCollection) + applyCollection(oohDeviceTypes, adcom1.DeviceOOH, deviceTypeMapCollection) +} + +// fiftyOneDtToRTB converts a 51Degrees device type to an OpenRTB device type. +// If the device type is not recognized, it defaults to PC. +func fiftyOneDtToRTB(val string) adcom1.DeviceType { + id, ok := deviceTypeMapCollection[deviceType(val)] + if ok { + return id + } + + return adcom1.DevicePC +} diff --git a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go new file mode 100644 index 00000000000..5fd0203bac8 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go @@ -0,0 +1,90 @@ +package devicedetection + +import ( + "testing" + + "github.com/prebid/openrtb/v20/adcom1" + "github.com/stretchr/testify/assert" +) + +func TestFiftyOneDtToRTB(t *testing.T) { + cases := []struct { + fiftyOneDt string + rtbDt adcom1.DeviceType + }{ + { + fiftyOneDt: "Phone", + rtbDt: adcom1.DevicePhone, + }, + { + fiftyOneDt: "Console", + rtbDt: adcom1.DeviceSetTopBox, + }, + { + fiftyOneDt: "Desktop", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "EReader", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "IoT", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "Kiosk", + rtbDt: adcom1.DeviceOOH, + }, + { + fiftyOneDt: "MediaHub", + rtbDt: adcom1.DeviceSetTopBox, + }, + { + fiftyOneDt: "Mobile", + rtbDt: adcom1.DeviceMobile, + }, + { + fiftyOneDt: "Router", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmallScreen", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmartPhone", + rtbDt: adcom1.DeviceMobile, + }, + { + fiftyOneDt: "SmartSpeaker", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmartWatch", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "Tablet", + rtbDt: adcom1.DeviceTablet, + }, + { + fiftyOneDt: "Tv", + rtbDt: adcom1.DeviceTV, + }, + { + fiftyOneDt: "Vehicle Display", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "Unknown", + rtbDt: adcom1.DevicePC, + }, + } + + for _, c := range cases { + t.Run(c.fiftyOneDt, func(t *testing.T) { + assert.Equal(t, c.rtbDt, fiftyOneDtToRTB(c.fiftyOneDt)) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go new file mode 100644 index 00000000000..911f20e1840 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go @@ -0,0 +1,27 @@ +package devicedetection + +import ( + "github.com/prebid/prebid-server/v2/hooks/hookexecution" + "github.com/prebid/prebid-server/v2/hooks/hookstage" +) + +// handleAuctionEntryPointRequestHook is a hookstage.HookFunc that is used to handle the auction entrypoint request hook. +func handleAuctionEntryPointRequestHook(cfg config, payload hookstage.EntrypointPayload, deviceDetector deviceDetector, evidenceExtractor evidenceExtractor, accountValidator accountValidator) (result hookstage.HookResult[hookstage.EntrypointPayload], err error) { + // if account/domain is not allowed, return failure + if !accountValidator.isAllowed(cfg, payload.Body) { + return hookstage.HookResult[hookstage.EntrypointPayload]{}, hookexecution.NewFailure("account not allowed") + } + // fetch evidence from headers and sua + evidenceFromHeaders := evidenceExtractor.fromHeaders(payload.Request, deviceDetector.getSupportedHeaders()) + evidenceFromSua := evidenceExtractor.fromSuaPayload(payload.Body) + + // create a Module context and set the evidence from headers, evidence from sua and dd enabled flag + moduleContext := make(hookstage.ModuleContext) + moduleContext[evidenceFromHeadersCtxKey] = evidenceFromHeaders + moduleContext[evidenceFromSuaCtxKey] = evidenceFromSua + moduleContext[ddEnabledCtxKey] = true + + return hookstage.HookResult[hookstage.EntrypointPayload]{ + ModuleContext: moduleContext, + }, nil +} diff --git a/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go b/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go new file mode 100644 index 00000000000..1146c3cc639 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go @@ -0,0 +1,173 @@ +package devicedetection + +import ( + "fmt" + "math" + + "github.com/prebid/prebid-server/v2/hooks/hookexecution" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +func handleAuctionRequestHook(ctx hookstage.ModuleInvocationContext, deviceDetector deviceDetector, evidenceExtractor evidenceExtractor) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { + var result hookstage.HookResult[hookstage.RawAuctionRequestPayload] + + // If the entrypoint hook was not configured, return the result without any changes + if ctx.ModuleContext == nil { + return result, hookexecution.NewFailure("entrypoint hook was not configured") + } + + result.ChangeSet.AddMutation( + func(rawPayload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) { + evidence, ua, err := evidenceExtractor.extract(ctx.ModuleContext) + if err != nil { + return rawPayload, hookexecution.NewFailure("error extracting evidence %s", err) + } + if evidence == nil { + return rawPayload, hookexecution.NewFailure("error extracting evidence") + } + + deviceInfo, err := deviceDetector.getDeviceInfo(evidence, ua) + if err != nil { + return rawPayload, hookexecution.NewFailure("error getting device info %s", err) + } + + result, err := hydrateFields(deviceInfo, rawPayload) + if err != nil { + return rawPayload, hookexecution.NewFailure(fmt.Sprintf("error hydrating fields %s", err)) + } + + return result, nil + }, hookstage.MutationUpdate, + ) + + return result, nil +} + +// hydrateFields hydrates the fields in the raw auction request payload with the device information +func hydrateFields(fiftyOneDd *deviceInfo, payload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) { + devicePayload := gjson.GetBytes(payload, "device") + dPV := devicePayload.Value() + if dPV == nil { + return payload, nil + } + + deviceObject := dPV.(map[string]any) + deviceObject = setMissingFields(deviceObject, fiftyOneDd) + deviceObject = signDeviceData(deviceObject, fiftyOneDd) + + return mergeDeviceIntoPayload(payload, deviceObject) +} + +// setMissingFields sets fields such as ["devicetype", "ua", "make", "os", "osv", "h", "w", "pxratio", "js", "geoFetch", "model", "ppi"] +// if they are not already present in the device object +func setMissingFields(deviceObj map[string]any, fiftyOneDd *deviceInfo) map[string]any { + optionalFields := map[string]func() any{ + "devicetype": func() any { + return fiftyOneDtToRTB(fiftyOneDd.DeviceType) + }, + "ua": func() any { + if fiftyOneDd.UserAgent != ddUnknown { + return fiftyOneDd.UserAgent + } + return nil + }, + "make": func() any { + if fiftyOneDd.HardwareVendor != ddUnknown { + return fiftyOneDd.HardwareVendor + } + return nil + }, + "os": func() any { + if fiftyOneDd.PlatformName != ddUnknown { + return fiftyOneDd.PlatformName + } + return nil + }, + "osv": func() any { + if fiftyOneDd.PlatformVersion != ddUnknown { + return fiftyOneDd.PlatformVersion + } + return nil + }, + "h": func() any { + return fiftyOneDd.ScreenPixelsHeight + }, + "w": func() any { + return fiftyOneDd.ScreenPixelsWidth + }, + "pxratio": func() any { + return fiftyOneDd.PixelRatio + }, + "js": func() any { + val := 0 + if fiftyOneDd.Javascript { + val = 1 + } + return val + }, + "geoFetch": func() any { + val := 0 + if fiftyOneDd.GeoLocation { + val = 1 + } + return val + }, + "model": func() any { + newVal := fiftyOneDd.HardwareModel + if newVal == ddUnknown { + newVal = fiftyOneDd.HardwareName + } + if newVal != ddUnknown { + return newVal + } + return nil + }, + "ppi": func() any { + if fiftyOneDd.ScreenPixelsHeight > 0 && fiftyOneDd.ScreenInchesHeight > 0 { + ppi := float64(fiftyOneDd.ScreenPixelsHeight) / fiftyOneDd.ScreenInchesHeight + return int(math.Round(ppi)) + } + return nil + }, + } + + for field, valFunc := range optionalFields { + _, ok := deviceObj[field] + if !ok { + val := valFunc() + if val != nil { + deviceObj[field] = val + } + } + } + + return deviceObj +} + +// signDeviceData signs the device data with the device information in the ext map of the device object +func signDeviceData(deviceObj map[string]any, fiftyOneDd *deviceInfo) map[string]any { + extObj, ok := deviceObj["ext"] + var ext map[string]any + if ok { + ext = extObj.(map[string]any) + } else { + ext = make(map[string]any) + } + + ext["fiftyonedegrees_deviceId"] = fiftyOneDd.DeviceId + deviceObj["ext"] = ext + + return deviceObj +} + +// mergeDeviceIntoPayload merges the modified device object back into the RawAuctionRequestPayload +func mergeDeviceIntoPayload(payload hookstage.RawAuctionRequestPayload, deviceObject map[string]any) (hookstage.RawAuctionRequestPayload, error) { + newPayload, err := sjson.SetBytes(payload, "device", deviceObject) + if err != nil { + return payload, err + } + + return newPayload, nil +} diff --git a/modules/fiftyonedegrees/devicedetection/models.go b/modules/fiftyonedegrees/devicedetection/models.go new file mode 100644 index 00000000000..c58daa211fd --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/models.go @@ -0,0 +1,66 @@ +package devicedetection + +// Prefixes in literal format +const queryPrefix = "query." +const headerPrefix = "header." +const ddUnknown = "Unknown" + +// Evidence where all fields are in string format +type stringEvidence struct { + Prefix string + Key string + Value string +} + +func getEvidenceByKey(e []stringEvidence, key string) (stringEvidence, bool) { + for _, evidence := range e { + if evidence.Key == key { + return evidence, true + } + } + return stringEvidence{}, false +} + +type deviceType string + +const ( + deviceTypePhone = "Phone" + deviceTypeConsole = "Console" + deviceTypeDesktop = "Desktop" + deviceTypeEReader = "EReader" + deviceTypeIoT = "IoT" + deviceTypeKiosk = "Kiosk" + deviceTypeMediaHub = "MediaHub" + deviceTypeMobile = "Mobile" + deviceTypeRouter = "Router" + deviceTypeSmallScreen = "SmallScreen" + deviceTypeSmartPhone = "SmartPhone" + deviceTypeSmartSpeaker = "SmartSpeaker" + deviceTypeSmartWatch = "SmartWatch" + deviceTypeTablet = "Tablet" + deviceTypeTv = "Tv" + deviceTypeVehicleDisplay = "Vehicle Display" +) + +type deviceInfo struct { + HardwareVendor string + HardwareName string + DeviceType string + PlatformVendor string + PlatformName string + PlatformVersion string + BrowserVendor string + BrowserName string + BrowserVersion string + ScreenPixelsWidth int64 + ScreenPixelsHeight int64 + PixelRatio float64 + Javascript bool + GeoLocation bool + HardwareFamily string + HardwareModel string + HardwareModelVariants string + UserAgent string + DeviceId string + ScreenInchesHeight float64 +} diff --git a/modules/fiftyonedegrees/devicedetection/models_test.go b/modules/fiftyonedegrees/devicedetection/models_test.go new file mode 100644 index 00000000000..898f25f4144 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/models_test.go @@ -0,0 +1,63 @@ +package devicedetection + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEvidenceByKey(t *testing.T) { + populatedEvidence := []stringEvidence{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key3", Value: "value3"}, + } + + tests := []struct { + name string + evidence []stringEvidence + key string + expectEvidence stringEvidence + expectFound bool + }{ + { + name: "nil_evidence", + evidence: nil, + key: "key2", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + { + name: "empty_evidence", + evidence: []stringEvidence{}, + key: "key2", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + { + name: "key_found", + evidence: populatedEvidence, + key: "key2", + expectEvidence: stringEvidence{ + Key: "key2", + Value: "value2", + }, + expectFound: true, + }, + { + name: "key_not_found", + evidence: populatedEvidence, + key: "key4", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, exists := getEvidenceByKey(test.evidence, test.key) + assert.Equal(t, test.expectFound, exists) + assert.Equal(t, test.expectEvidence, result) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/module.go b/modules/fiftyonedegrees/devicedetection/module.go new file mode 100644 index 00000000000..df72e6338a5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/module.go @@ -0,0 +1,107 @@ +package devicedetection + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/modules/moduledeps" +) + +func configHashFromConfig(cfg *config) *dd.ConfigHash { + configHash := dd.NewConfigHash(cfg.getPerformanceProfile()) + if cfg.Performance.Concurrency != nil { + configHash.SetConcurrency(uint16(*cfg.Performance.Concurrency)) + } + + if cfg.Performance.Difference != nil { + configHash.SetDifference(int32(*cfg.Performance.Difference)) + } + + if cfg.Performance.AllowUnmatched != nil { + configHash.SetAllowUnmatched(*cfg.Performance.AllowUnmatched) + } + + if cfg.Performance.Drift != nil { + configHash.SetDrift(int32(*cfg.Performance.Drift)) + } + return configHash +} + +func Builder(rawConfig json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) { + cfg, err := parseConfig(rawConfig) + if err != nil { + return Module{}, errors.Wrap(err, "failed to parse config") + } + + err = validateConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "invalid config") + } + + configHash := configHashFromConfig(&cfg) + + deviceDetectorImpl, err := newDeviceDetector( + configHash, + &cfg, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create device detector") + } + + return Module{ + cfg, + deviceDetectorImpl, + newEvidenceExtractor(), + newAccountValidator(), + }, + nil +} + +type Module struct { + config config + deviceDetector deviceDetector + evidenceExtractor evidenceExtractor + accountValidator accountValidator +} + +type deviceDetector interface { + getSupportedHeaders() []dd.EvidenceKey + getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) +} + +type accountValidator interface { + isAllowed(cfg config, req []byte) bool +} + +type evidenceExtractor interface { + fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence + fromSuaPayload(payload []byte) []stringEvidence + extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) +} + +func (m Module) HandleEntrypointHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + payload hookstage.EntrypointPayload, +) (hookstage.HookResult[hookstage.EntrypointPayload], error) { + return handleAuctionEntryPointRequestHook( + m.config, + payload, + m.deviceDetector, + m.evidenceExtractor, + m.accountValidator, + ) +} + +func (m Module) HandleRawAuctionHook( + _ context.Context, + mCtx hookstage.ModuleInvocationContext, + _ hookstage.RawAuctionRequestPayload, +) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { + return handleAuctionRequestHook(mCtx, m.deviceDetector, m.evidenceExtractor) +} diff --git a/modules/fiftyonedegrees/devicedetection/module_test.go b/modules/fiftyonedegrees/devicedetection/module_test.go new file mode 100644 index 00000000000..7b8095ac431 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/module_test.go @@ -0,0 +1,703 @@ +package devicedetection + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "os" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/modules/moduledeps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockAccValidator struct { + mock.Mock +} + +func (m *mockAccValidator) isAllowed(cfg config, req []byte) bool { + args := m.Called(cfg, req) + return args.Bool(0) +} + +type mockEvidenceExtractor struct { + mock.Mock +} + +func (m *mockEvidenceExtractor) fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + args := m.Called(request, httpHeaderKeys) + + return args.Get(0).([]stringEvidence) +} + +func (m *mockEvidenceExtractor) fromSuaPayload(payload []byte) []stringEvidence { + args := m.Called(payload) + + return args.Get(0).([]stringEvidence) +} + +func (m *mockEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { + args := m.Called(ctx) + + res := args.Get(0) + if res == nil { + return nil, args.String(1), args.Error(2) + } + + return res.([]onpremise.Evidence), args.String(1), args.Error(2) +} + +type mockDeviceDetector struct { + mock.Mock +} + +func (m *mockDeviceDetector) getSupportedHeaders() []dd.EvidenceKey { + args := m.Called() + return args.Get(0).([]dd.EvidenceKey) +} + +func (m *mockDeviceDetector) getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) { + + args := m.Called(evidence, ua) + + res := args.Get(0) + + if res == nil { + return nil, args.Error(1) + } + + return res.(*deviceInfo), args.Error(1) +} + +func TestHandleEntrypointHookAccountNotAllowed(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(false) + + module := Module{ + accountValidator: &mockValidator, + } + + _, err := module.HandleEntrypointHook(nil, hookstage.ModuleInvocationContext{}, hookstage.EntrypointPayload{}) + assert.Error(t, err) + assert.Equal(t, "hook execution failed: account not allowed", err.Error()) +} + +func TestHandleEntrypointHookAccountAllowed(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("fromHeaders", mock.Anything, mock.Anything).Return( + []stringEvidence{{ + Prefix: "123", + Key: "key", + Value: "val", + }}, + ) + + mockEvidenceExtractor.On("fromSuaPayload", mock.Anything, mock.Anything).Return( + []stringEvidence{{ + Prefix: "123", + Key: "User-Agent", + Value: "ua", + }}, + ) + + var mockDeviceDetector mockDeviceDetector + + mockDeviceDetector.On("getSupportedHeaders").Return( + []dd.EvidenceKey{{ + Prefix: dd.HttpEvidenceQuery, + Key: "key", + }}, + ) + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + result, err := module.HandleEntrypointHook(nil, hookstage.ModuleInvocationContext{}, hookstage.EntrypointPayload{}) + assert.NoError(t, err) + + assert.Equal( + t, result.ModuleContext[evidenceFromHeadersCtxKey], []stringEvidence{{ + Prefix: "123", + Key: "key", + Value: "val", + }}, + ) + + assert.Equal( + t, result.ModuleContext[evidenceFromSuaCtxKey], []stringEvidence{{ + Prefix: "123", + Key: "User-Agent", + Value: "ua", + }}, + ) +} + +func TestHandleRawAuctionHookNoCtx(t *testing.T) { + module := Module{} + + _, err := module.HandleRawAuctionHook( + nil, + hookstage.ModuleInvocationContext{}, + hookstage.RawAuctionRequestPayload{}, + ) + assert.Errorf(t, err, "entrypoint hook was not configured") +} + +func TestHandleRawAuctionHookExtractError(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var evidenceExtractorM mockEvidenceExtractor + evidenceExtractorM.On("extract", mock.Anything).Return( + nil, + "ua", + nil, + ) + + var mockDeviceDetector mockDeviceDetector + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &evidenceExtractorM, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + context.TODO(), hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + hookstage.RawAuctionRequestPayload{}, + ) + + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + body := []byte(`{}`) + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error extracting evidence") + + var mockEvidenceErrExtractor mockEvidenceExtractor + mockEvidenceErrExtractor.On("extract", mock.Anything).Return( + nil, + "", + errors.New("error"), + ) + + module.evidenceExtractor = &mockEvidenceErrExtractor + + result, err = module.HandleRawAuctionHook( + context.TODO(), hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + hookstage.RawAuctionRequestPayload{}, + ) + + assert.NoError(t, err) + + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation = result.ChangeSet.Mutations()[0] + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error extracting evidence error") + +} + +func TestHandleRawAuctionHookEnrichment(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("extract", mock.Anything).Return( + []onpremise.Evidence{ + { + Key: "key", + Value: "val", + }, + }, + "ua", + nil, + ) + + var deviceDetectorM mockDeviceDetector + + deviceDetectorM.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "", + }, + nil, + ) + + module := Module{ + deviceDetector: &deviceDetectorM, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + body := []byte(`{ + "device": { + "connectiontype": 2, + "ext": { + "atts": 0, + "ifv": "1B8EFA09-FF8F-4123-B07F-7283B50B3870" + }, + "sua": { + "source": 2, + "browsers": [ + { + "brand": "Not A(Brand", + "version": [ + "99", + "0", + "0", + "0" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "121", + "0", + "6167", + "184" + ] + }, + { + "brand": "Chromium", + "version": [ + "121", + "0", + "6167", + "184" + ] + } + ], + "platform": { + "brand": "macOS", + "version": [ + "14", + "0", + "0" + ] + }, + "mobile": 0, + "architecture": "arm", + "model": "" + } + } + }`) + + mutationResult, err := mutation.Apply(body) + + require.JSONEq(t, string(mutationResult), `{ + "device": { + "connectiontype": 2, + "ext": { + "atts": 0, + "ifv": "1B8EFA09-FF8F-4123-B07F-7283B50B3870", + "fiftyonedegrees_deviceId":"" + }, + "sua": { + "source": 2, + "browsers": [ + { + "brand": "Not A(Brand", + "version": [ + "99", + "0", + "0", + "0" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "121", + "0", + "6167", + "184" + ] + }, + { + "brand": "Chromium", + "version": [ + "121", + "0", + "6167", + "184" + ] + } + ], + "platform": { + "brand": "macOS", + "version": [ + "14", + "0", + "0" + ] + }, + "mobile": 0, + "architecture": "arm", + "model": "" + } + ,"devicetype":2,"ua":"ua","make":"Apple","model":"Macbook","os":"MacOs","osv":"14","h":1080,"w":1024,"pxratio":223,"js":1,"geoFetch":1} + }`) + + var deviceDetectorErrM mockDeviceDetector + + deviceDetectorErrM.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + nil, + errors.New("error"), + ) + + module.deviceDetector = &deviceDetectorErrM + + result, err = module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + + assert.NoError(t, err) + + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation = result.ChangeSet.Mutations()[0] + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error getting device info") +} + +func TestHandleRawAuctionHookEnrichmentWithErrors(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("extract", mock.Anything).Return( + []onpremise.Evidence{ + { + Key: "key", + Value: "val", + }, + }, + "ua", + nil, + ) + + var mockDeviceDetector mockDeviceDetector + + mockDeviceDetector.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "", + ScreenInchesHeight: 7, + }, + nil, + ) + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + mutationResult, err := mutation.Apply(hookstage.RawAuctionRequestPayload(`{"device":{}}`)) + assert.NoError(t, err) + require.JSONEq(t, string(mutationResult), `{"device":{"devicetype":2,"ua":"ua","make":"Apple","model":"Macbook","os":"MacOs","osv":"14","h":1080,"w":1024,"pxratio":223,"js":1,"geoFetch":1,"ppi":154,"ext":{"fiftyonedegrees_deviceId":""}}}`) +} + +func TestConfigHashFromConfig(t *testing.T) { + cfg := config{ + Performance: performance{ + Profile: "", + Concurrency: nil, + Difference: nil, + AllowUnmatched: nil, + Drift: nil, + }, + } + + result := configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.Default) + assert.Equal(t, result.Concurrency(), uint16(0xa)) + assert.Equal(t, result.Difference(), int32(0)) + assert.Equal(t, result.AllowUnmatched(), false) + assert.Equal(t, result.Drift(), int32(0)) + + concurrency := 1 + difference := 1 + allowUnmatched := true + drift := 1 + + cfg = config{ + Performance: performance{ + Profile: "Balanced", + Concurrency: &concurrency, + Difference: &difference, + AllowUnmatched: &allowUnmatched, + Drift: &drift, + }, + } + + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.Balanced) + assert.Equal(t, result.Concurrency(), uint16(1)) + assert.Equal(t, result.Difference(), int32(1)) + assert.Equal(t, result.AllowUnmatched(), true) + assert.Equal(t, result.Drift(), int32(1)) + + cfg = config{ + Performance: performance{ + Profile: "InMemory", + }, + } + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.InMemory) + + cfg = config{ + Performance: performance{ + Profile: "HighPerformance", + }, + } + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.HighPerformance) +} + +func TestSignDeviceData(t *testing.T) { + devicePld := map[string]any{ + "ext": map[string]any{ + "my-key": "my-value", + }, + } + + deviceInfo := deviceInfo{ + DeviceId: "test-device-id", + } + + result := signDeviceData(devicePld, &deviceInfo) + r, err := json.Marshal(result) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + require.JSONEq( + t, + `{"ext":{"fiftyonedegrees_deviceId":"test-device-id","my-key":"my-value"}}`, + string(r), + ) +} + +func TestBuilderWithInvalidJson(t *testing.T) { + _, err := Builder([]byte(`{`), moduledeps.ModuleDeps{}) + assert.Error(t, err) + assert.Errorf(t, err, "failed to parse config") +} + +func TestBuilderWithInvalidConfig(t *testing.T) { + _, err := Builder([]byte(`{"data_file":{}}`), moduledeps.ModuleDeps{}) + assert.Error(t, err) + assert.Errorf(t, err, "invalid config") +} + +func TestBuilderHandleDeviceDetectorError(t *testing.T) { + var mockConfig config + mockConfig.Performance.Profile = "default" + testFile, _ := os.Create("test-builder-config.hash") + defer testFile.Close() + defer os.Remove("test-builder-config.hash") + + _, err := Builder( + []byte(`{ + "enabled": true, + "data_file": { + "path": "test-builder-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "123", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), moduledeps.ModuleDeps{}, + ) + assert.Error(t, err) + assert.Errorf(t, err, "failed to create device detector") +} + +func TestHydrateFields(t *testing.T) { + deviceInfo := &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "dev-ide", + } + + rawPld := `{ + "imp": [{ + "id": "", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + }], + "device": { + "model": "Macintosh", + "w": 843, + "h": 901, + "dnt": 0, + "ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A037U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36", + "language": "en", + "sua": {"browsers":[{"brand":"Not/A)Brand","version":["99","0","0","0"]},{"brand":"Samsung Internet","version":["23","0","1","1"]},{"brand":"Chromium","version":["115","0","5790","168"]}],"platform":{"brand":"Android","version":["13","0","0"]},"mobile":1,"model":"SM-A037U","source":2}, + "ext": {"h":"901","w":843} + }, + "cur": [ + "USD" + ], + "tmax": 1700 + }` + + payload, err := hydrateFields(deviceInfo, []byte(rawPld)) + assert.NoError(t, err) + + var deviceHolder struct { + Device json.RawMessage `json:"device"` + } + + err = json.Unmarshal(payload, &deviceHolder) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + require.JSONEq( + t, + `{"devicetype":2,"dnt":0,"ext":{"fiftyonedegrees_deviceId":"dev-ide","h":"901","w":843},"geoFetch":1,"h":901,"js":1,"language":"en","make":"Apple","model":"Macintosh","os":"MacOs","osv":"14","pxratio":223,"sua":{"browsers":[{"brand":"Not/A)Brand","version":["99","0","0","0"]},{"brand":"Samsung Internet","version":["23","0","1","1"]},{"brand":"Chromium","version":["115","0","5790","168"]}],"mobile":1,"model":"SM-A037U","platform":{"brand":"Android","version":["13","0","0"]},"source":2},"ua":"Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A037U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36","w":843}`, + string(deviceHolder.Device), + ) +} diff --git a/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go b/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go new file mode 100644 index 00000000000..8440886b353 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go @@ -0,0 +1,47 @@ +package devicedetection + +import ( + "net/http" + "strings" + + "github.com/51Degrees/device-detection-go/v4/dd" +) + +// evidenceFromRequestHeadersExtractor is a struct that extracts evidence from http request headers +type evidenceFromRequestHeadersExtractor struct{} + +func newEvidenceFromRequestHeadersExtractor() evidenceFromRequestHeadersExtractor { + return evidenceFromRequestHeadersExtractor{} +} + +func (x evidenceFromRequestHeadersExtractor) extract(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + return x.extractEvidenceStrings(request, httpHeaderKeys) +} + +func (x evidenceFromRequestHeadersExtractor) extractEvidenceStrings(r *http.Request, keys []dd.EvidenceKey) []stringEvidence { + evidence := make([]stringEvidence, 0) + for _, e := range keys { + if e.Prefix == dd.HttpEvidenceQuery { + continue + } + + // Get evidence from headers + headerVal := r.Header.Get(e.Key) + if headerVal == "" { + continue + } + + if e.Key != secUaFullVersionList && e.Key != secChUa { + headerVal = strings.Replace(headerVal, "\"", "", -1) + } + + if headerVal != "" { + evidence = append(evidence, stringEvidence{ + Prefix: headerPrefix, + Key: e.Key, + Value: headerVal, + }) + } + } + return evidence +} diff --git a/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go b/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go new file mode 100644 index 00000000000..77fbed3a42f --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go @@ -0,0 +1,118 @@ +package devicedetection + +import ( + "net/http" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/stretchr/testify/assert" +) + +func TestExtractEvidenceStrings(t *testing.T) { + tests := []struct { + name string + headers map[string]string + keys []dd.EvidenceKey + expectedEvidence []stringEvidence + }{ + { + name: "Ignored_query_evidence", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpEvidenceQuery, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{}, + }, + { + name: "Empty_headers", + headers: map[string]string{}, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{}, + }, + { + name: "Single_header", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "User-Agent", Value: "Mozilla/5.0"}, + }, + }, + { + name: "Multiple_headers", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + "Accept": "text/html", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + {Prefix: dd.HttpEvidenceQuery, Key: "Query"}, + {Prefix: dd.HttpHeaderString, Key: "Accept"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "User-Agent", Value: "Mozilla/5.0"}, + {Prefix: headerPrefix, Key: "Accept", Value: "text/html"}, + }, + }, + { + name: "Header_with_quotes_removed", + headers: map[string]string{ + "IP-List": "\"92.0.4515.159\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "IP-List"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "IP-List", Value: "92.0.4515.159"}, + }, + }, + { + name: "Sec-CH-UA_headers_with_quotes_left", + headers: map[string]string{ + "Sec-CH-UA": "\"Chromium\";v=\"92\", \"Google Chrome\";v=\"92\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: secChUa}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: secChUa, Value: "\"Chromium\";v=\"92\", \"Google Chrome\";v=\"92\""}, + }, + }, + { + name: "Sec-CH-UA-Full-Version-List_headers_with_quotes_left", + headers: map[string]string{ + "Sec-CH-UA-Full-Version-List": "\"92.0.4515.159\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: secUaFullVersionList}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: secUaFullVersionList, Value: "\"92.0.4515.159\""}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := http.Request{ + Header: make(map[string][]string), + } + + for key, value := range test.headers { + req.Header.Set(key, value) + } + + extractor := newEvidenceFromRequestHeadersExtractor() + evidence := extractor.extractEvidenceStrings(&req, test.keys) + + assert.Equal(t, test.expectedEvidence, evidence) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/sample/pbs.json b/modules/fiftyonedegrees/devicedetection/sample/pbs.json new file mode 100644 index 00000000000..43fd28610f1 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sample/pbs.json @@ -0,0 +1,84 @@ +{ + "adapters": [ + { + "appnexus": { + "enabled": true + } + } + ], + "gdpr": { + "enabled": true, + "default_value": 0, + "timeouts_ms": { + "active_vendorlist_fetch": 900000 + } + }, + "stored_requests": { + "filesystem": { + "enabled": true, + "directorypath": "sample/stored" + } + }, + "stored_responses": { + "filesystem": { + "enabled": true, + "directorypath": "sample/stored" + } + }, + "hooks": { + "enabled": true, + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "data_file": { + "path": "TAC-HashV41.hash", + "update": { + "auto": false, + "polling_interval": 3600, + "license_key": "YOUR_LICENSE_KEY", + "product": "V4Enterprise" + } + }, + "performance": { + "profile": "InMemory" + } + } + } + }, + "host_execution_plan": { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-raw-auction-request-hook" + } + ] + } + ] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/fiftyonedegrees/devicedetection/sample/request_data.json b/modules/fiftyonedegrees/devicedetection/sample/request_data.json new file mode 100644 index 00000000000..1f6bc8900f8 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sample/request_data.json @@ -0,0 +1,114 @@ +{ + "imp": [{ + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "test" + }, + "pbadslot": "test", + "gpid": "test" + }, + "gpid": "test", + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 1, + "use_pmt_rule": false + } + }, + "adunitcode": "25e8ad9f-13a4-4404-ba74-f9eebff0e86c", + "floors": { + "floorMin": 0.01 + } + } + }, + "id": "2529eeea-813e-4da6-838f-f91c28d64867", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + }], + "site": { + "domain": "test.com", + "publisher": { + "domain": "test.com", + "id": "1" + }, + "page": "https://www.test.com/" + }, + "device": { + "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36" + }, + "id": "fc4670ce-4985-4316-a245-b43c885dc37a", + "test": 1, + "cur": [ + "USD" + ], + "source": { + "ext": { + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + }, + "ext": { + "prebid": { + "cache": { + "bids": { + "returnCreative": true + }, + "vastxml": { + "returnCreative": true + } + }, + "auctiontimestamp": 1698390609882, + "targeting": { + "includewinners": true, + "includebidderkeys": false + }, + "schains": [ + { + "bidders": [ + "appnexus" + ], + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + ], + "floors": { + "enabled": false, + "floorMin": 0.01, + "floorMinCur": "USD" + }, + "createtids": false + } + }, + "user": {}, + "tmax": 1700 +} \ No newline at end of file diff --git a/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go b/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go new file mode 100644 index 00000000000..ab69210449f --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go @@ -0,0 +1,144 @@ +package devicedetection + +import ( + "fmt" + "strings" + + "github.com/spf13/cast" + "github.com/tidwall/gjson" +) + +const ( + secChUaArch = "Sec-Ch-Ua-Arch" + secChUaMobile = "Sec-Ch-Ua-Mobile" + secChUaModel = "Sec-Ch-Ua-Model" + secChUaPlatform = "Sec-Ch-Ua-Platform" + secUaFullVersionList = "Sec-Ch-Ua-Full-Version-List" + secChUaPlatformVersion = "Sec-Ch-Ua-Platform-Version" + secChUa = "Sec-Ch-Ua" + + userAgentHeader = "User-Agent" +) + +// evidenceFromSUAPayloadExtractor extracts evidence from the SUA payload of device +type evidenceFromSUAPayloadExtractor struct{} + +func newEvidenceFromSUAPayloadExtractor() evidenceFromSUAPayloadExtractor { + return evidenceFromSUAPayloadExtractor{} +} + +// Extract extracts evidence from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extract(payload []byte) []stringEvidence { + if payload != nil { + return x.extractEvidenceStrings(payload) + } + + return nil +} + +var ( + uaPath = "device.ua" + archPath = "device.sua.architecture" + mobilePath = "device.sua.mobile" + modelPath = "device.sua.model" + platformBrandPath = "device.sua.platform.brand" + platformVersionPath = "device.sua.platform.version" + browsersPath = "device.sua.browsers" +) + +// extractEvidenceStrings extracts evidence from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extractEvidenceStrings(payload []byte) []stringEvidence { + res := make([]stringEvidence, 0, 10) + + uaResult := gjson.GetBytes(payload, uaPath) + if uaResult.Exists() { + res = append( + res, + stringEvidence{Prefix: headerPrefix, Key: userAgentHeader, Value: uaResult.String()}, + ) + } + + archResult := gjson.GetBytes(payload, archPath) + if archResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaArch, archResult.String()) + } + + mobileResult := gjson.GetBytes(payload, mobilePath) + if mobileResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaMobile, mobileResult.String()) + } + + modelResult := gjson.GetBytes(payload, modelPath) + if modelResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaModel, modelResult.String()) + } + + platformBrandResult := gjson.GetBytes(payload, platformBrandPath) + if platformBrandResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaPlatform, platformBrandResult.String()) + } + + platformVersionResult := gjson.GetBytes(payload, platformVersionPath) + if platformVersionResult.Exists() { + res = x.appendEvidenceIfExists( + res, + secChUaPlatformVersion, + strings.Join(resultToStringArray(platformVersionResult.Array()), "."), + ) + } + + browsersResult := gjson.GetBytes(payload, browsersPath) + if browsersResult.Exists() { + res = x.appendEvidenceIfExists(res, secUaFullVersionList, x.extractBrowsers(browsersResult)) + + } + + return res +} + +func resultToStringArray(array []gjson.Result) []string { + strArray := make([]string, len(array)) + for i, result := range array { + strArray[i] = result.String() + } + + return strArray +} + +// appendEvidenceIfExists appends evidence to the destination if the value is not nil +func (x evidenceFromSUAPayloadExtractor) appendEvidenceIfExists(destination []stringEvidence, name string, value interface{}) []stringEvidence { + if value != nil { + valStr := cast.ToString(value) + if len(valStr) == 0 { + return destination + } + + return append( + destination, + stringEvidence{Prefix: headerPrefix, Key: name, Value: valStr}, + ) + } + + return destination +} + +// extractBrowsers extracts browsers from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extractBrowsers(browsers gjson.Result) string { + if !browsers.IsArray() { + return "" + } + + browsersRaw := make([]string, len(browsers.Array())) + + for i, result := range browsers.Array() { + brand := result.Get("brand").String() + versionsRaw := result.Get("version").Array() + versions := resultToStringArray(versionsRaw) + + browsersRaw[i] = fmt.Sprintf(`"%s";v="%s"`, brand, strings.Join(versions, ".")) + } + + res := strings.Join(browsersRaw, ",") + + return res +} diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 173120f7301..f7706ce5c25 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -29,6 +29,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdkernel, BidderAdkernelAdn, BidderAdman, + BidderAdmatic, BidderAdmixer, BidderAdnuntius, BidderAdOcean, @@ -42,6 +43,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdtarget, BidderAdtrgtme, BidderAdtelligent, + BidderAdTonos, BidderAdvangelists, BidderAdView, BidderAdxcg, @@ -54,6 +56,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderApacdex, BidderAppnexus, BidderAppush, + BidderAso, BidderAudienceNetwork, BidderAutomatad, BidderAvocet, @@ -65,10 +68,12 @@ var coreBidderNames []BidderName = []BidderName{ BidderBetween, BidderBeyondMedia, BidderBidmachine, + BidderBidmatic, BidderBidmyadz, BidderBidsCube, BidderBidstack, - BidderBizzclick, + BidderBigoAd, + BidderBlasto, BidderBliink, BidderBlue, BidderBluesea, @@ -78,12 +83,15 @@ var coreBidderNames []BidderName = []BidderName{ BidderBWX, BidderCadentApertureMX, BidderCcx, + BidderCointraffic, BidderCoinzilla, BidderColossus, BidderCompass, + BidderConcert, BidderConnectAd, BidderConsumable, BidderConversant, + BidderCopper6ssp, BidderCpmstar, BidderCriteo, BidderCWire, @@ -92,13 +100,16 @@ var coreBidderNames []BidderName = []BidderName{ BidderDeepintent, BidderDefinemedia, BidderDianomi, + BidderDisplayio, BidderEdge226, BidderDmx, BidderDXKulture, + BidderDriftPixel, BidderEmtv, BidderEmxDigital, BidderEPlanning, BidderEpom, + BidderEscalax, BidderEVolution, BidderFlipp, BidderFreewheelSSP, @@ -128,20 +139,24 @@ var coreBidderNames []BidderName = []BidderName{ BidderLmKiviads, BidderKrushmedia, BidderLemmadigital, - BidderLiftoff, BidderLimelightDigital, BidderLockerDome, BidderLogan, BidderLogicad, + BidderLoyal, BidderLunaMedia, BidderMabidder, BidderMadvertise, BidderMarsmedia, BidderMediafuse, + BidderMediaGo, BidderMedianet, + BidderMeloZen, + BidderMetaX, BidderMgid, BidderMgidX, BidderMinuteMedia, + BidderMissena, BidderMobfoxpb, BidderMobileFuse, BidderMotorik, @@ -152,19 +167,25 @@ var coreBidderNames []BidderName = []BidderName{ BidderOpenWeb, BidderOpenx, BidderOperaads, + BidderOraki, BidderOrbidder, BidderOutbrain, BidderOwnAdx, BidderPangle, BidderPGAMSsp, + BidderPlaydigo, BidderPubmatic, + BidderPubrise, BidderPubnative, BidderPulsepoint, BidderPWBid, + BidderQT, + BidderReadpeak, BidderRelevantDigital, BidderRevcontent, BidderRichaudience, BidderRise, + BidderRoulax, BidderRTBHouse, BidderRubicon, BidderSeedingAlliance, @@ -180,6 +201,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderSmartx, BidderSmartyAds, BidderSmileWanted, + BidderSmrtconnect, BidderSonobi, BidderSovrn, BidderSovrnXsp, @@ -189,14 +211,18 @@ var coreBidderNames []BidderName = []BidderName{ BidderTappx, BidderTeads, BidderTelaria, + BidderTheadx, + BidderTheTradeDesk, BidderTpmn, BidderTrafficGate, BidderTriplelift, BidderTripleliftNative, + BidderTrustedstack, BidderUcfunnel, BidderUndertone, BidderUnicorn, BidderUnruly, + BidderVidazoo, BidderVideoByte, BidderVideoHeroes, BidderVidoomy, @@ -204,6 +230,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderVisx, BidderVox, BidderVrtcal, + BidderVungle, BidderXeworks, BidderYahooAds, BidderYandex, @@ -292,6 +319,28 @@ func IsBidderNameReserved(name string) bool { return false } +// IsPotentialBidder returns true if the name is not reserved witbin the imp[].ext context +func IsPotentialBidder(name string) bool { + switch BidderName(name) { + case BidderReservedContext: + return false + case BidderReservedData: + return false + case BidderReservedGPID: + return false + case BidderReservedPrebid: + return false + case BidderReservedSKAdN: + return false + case BidderReservedTID: + return false + case BidderReservedAE: + return false + default: + return true + } +} + // Names of core bidders. These names *must* match the bidder code in Prebid.js if an adapter also exists in that // project. You may *not* use the name 'general' as that is reserved for general error messages nor 'context' as // that is reserved for first party data. @@ -309,6 +358,7 @@ const ( BidderAdkernel BidderName = "adkernel" BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdman BidderName = "adman" + BidderAdmatic BidderName = "admatic" BidderAdmixer BidderName = "admixer" BidderAdnuntius BidderName = "adnuntius" BidderAdOcean BidderName = "adocean" @@ -321,6 +371,7 @@ const ( BidderAdsinteractive BidderName = "adsinteractive" BidderAdtarget BidderName = "adtarget" BidderAdtrgtme BidderName = "adtrgtme" + BidderAdTonos BidderName = "adtonos" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAdView BidderName = "adview" @@ -334,6 +385,7 @@ const ( BidderApacdex BidderName = "apacdex" BidderAppnexus BidderName = "appnexus" BidderAppush BidderName = "appush" + BidderAso BidderName = "aso" BidderAudienceNetwork BidderName = "audienceNetwork" BidderAutomatad BidderName = "automatad" BidderAvocet BidderName = "avocet" @@ -345,10 +397,12 @@ const ( BidderBetween BidderName = "between" BidderBeyondMedia BidderName = "beyondmedia" BidderBidmachine BidderName = "bidmachine" + BidderBidmatic BidderName = "bidmatic" BidderBidmyadz BidderName = "bidmyadz" BidderBidsCube BidderName = "bidscube" BidderBidstack BidderName = "bidstack" - BidderBizzclick BidderName = "bizzclick" + BidderBigoAd BidderName = "bigoad" + BidderBlasto BidderName = "blasto" BidderBliink BidderName = "bliink" BidderBlue BidderName = "blue" BidderBluesea BidderName = "bluesea" @@ -358,12 +412,15 @@ const ( BidderBWX BidderName = "bwx" BidderCadentApertureMX BidderName = "cadent_aperture_mx" BidderCcx BidderName = "ccx" + BidderCointraffic BidderName = "cointraffic" BidderCoinzilla BidderName = "coinzilla" BidderColossus BidderName = "colossus" BidderCompass BidderName = "compass" + BidderConcert BidderName = "concert" BidderConnectAd BidderName = "connectad" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" + BidderCopper6ssp BidderName = "copper6ssp" BidderCpmstar BidderName = "cpmstar" BidderCriteo BidderName = "criteo" BidderCWire BidderName = "cwire" @@ -372,13 +429,16 @@ const ( BidderDeepintent BidderName = "deepintent" BidderDefinemedia BidderName = "definemedia" BidderDianomi BidderName = "dianomi" + BidderDisplayio BidderName = "displayio" BidderEdge226 BidderName = "edge226" BidderDmx BidderName = "dmx" BidderDXKulture BidderName = "dxkulture" + BidderDriftPixel BidderName = "driftpixel" BidderEmtv BidderName = "emtv" BidderEmxDigital BidderName = "emx_digital" BidderEPlanning BidderName = "eplanning" BidderEpom BidderName = "epom" + BidderEscalax BidderName = "escalax" BidderEVolution BidderName = "e_volution" BidderFlipp BidderName = "flipp" BidderFreewheelSSP BidderName = "freewheelssp" @@ -408,20 +468,24 @@ const ( BidderLmKiviads BidderName = "lm_kiviads" BidderKrushmedia BidderName = "krushmedia" BidderLemmadigital BidderName = "lemmadigital" - BidderLiftoff BidderName = "liftoff" BidderLimelightDigital BidderName = "limelightDigital" BidderLockerDome BidderName = "lockerdome" BidderLogan BidderName = "logan" BidderLogicad BidderName = "logicad" + BidderLoyal BidderName = "loyal" BidderLunaMedia BidderName = "lunamedia" BidderMabidder BidderName = "mabidder" BidderMadvertise BidderName = "madvertise" BidderMarsmedia BidderName = "marsmedia" BidderMediafuse BidderName = "mediafuse" + BidderMediaGo BidderName = "mediago" BidderMedianet BidderName = "medianet" + BidderMeloZen BidderName = "melozen" + BidderMetaX BidderName = "metax" BidderMgid BidderName = "mgid" BidderMgidX BidderName = "mgidX" BidderMinuteMedia BidderName = "minutemedia" + BidderMissena BidderName = "missena" BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" BidderMotorik BidderName = "motorik" @@ -432,19 +496,25 @@ const ( BidderOpenWeb BidderName = "openweb" BidderOpenx BidderName = "openx" BidderOperaads BidderName = "operaads" + BidderOraki BidderName = "oraki" BidderOrbidder BidderName = "orbidder" BidderOutbrain BidderName = "outbrain" BidderOwnAdx BidderName = "ownadx" BidderPangle BidderName = "pangle" BidderPGAMSsp BidderName = "pgamssp" + BidderPlaydigo BidderName = "playdigo" BidderPubmatic BidderName = "pubmatic" + BidderPubrise BidderName = "pubrise" BidderPubnative BidderName = "pubnative" BidderPulsepoint BidderName = "pulsepoint" BidderPWBid BidderName = "pwbid" + BidderQT BidderName = "qt" + BidderReadpeak BidderName = "readpeak" BidderRelevantDigital BidderName = "relevantdigital" BidderRevcontent BidderName = "revcontent" BidderRichaudience BidderName = "richaudience" BidderRise BidderName = "rise" + BidderRoulax BidderName = "roulax" BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSeedingAlliance BidderName = "seedingAlliance" @@ -460,6 +530,7 @@ const ( BidderSmartx BidderName = "smartx" BidderSmartyAds BidderName = "smartyads" BidderSmileWanted BidderName = "smilewanted" + BidderSmrtconnect BidderName = "smrtconnect" BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" BidderSovrnXsp BidderName = "sovrnXsp" @@ -469,14 +540,18 @@ const ( BidderTappx BidderName = "tappx" BidderTeads BidderName = "teads" BidderTelaria BidderName = "telaria" + BidderTheadx BidderName = "theadx" + BidderTheTradeDesk BidderName = "thetradedesk" BidderTpmn BidderName = "tpmn" BidderTrafficGate BidderName = "trafficgate" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" + BidderTrustedstack BidderName = "trustedstack" BidderUcfunnel BidderName = "ucfunnel" BidderUndertone BidderName = "undertone" BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" + BidderVidazoo BidderName = "vidazoo" BidderVideoByte BidderName = "videobyte" BidderVideoHeroes BidderName = "videoheroes" BidderVidoomy BidderName = "vidoomy" @@ -484,6 +559,7 @@ const ( BidderVisx BidderName = "visx" BidderVox BidderName = "vox" BidderVrtcal BidderName = "vrtcal" + BidderVungle BidderName = "vungle" BidderXeworks BidderName = "xeworks" BidderYahooAds BidderName = "yahooAds" BidderYandex BidderName = "yandex" @@ -539,6 +615,8 @@ var bidderNameLookup = func() map[string]BidderName { return lookup }() +type BidderNameNormalizer func(name string) (BidderName, bool) + func NormalizeBidderName(name string) (BidderName, bool) { nameLower := strings.ToLower(name) bidderName, exists := bidderNameLookup[nameLower] @@ -612,6 +690,7 @@ func NewBidderParamsValidator(schemaDirectory string) (BidderParamValidator, err if _, ok := bidderMap[bidderName]; !ok { return nil, fmt.Errorf("File %s/%s does not match a valid BidderName.", schemaDirectory, fileInfo.Name()) } + toOpen, err := paramsValidator.abs(filepath.Join(schemaDirectory, fileInfo.Name())) if err != nil { return nil, fmt.Errorf("Failed to get an absolute representation of the path: %s, %v", toOpen, err) diff --git a/openrtb_ext/convert_down.go b/openrtb_ext/convert_down.go index e0842978551..bfb6028d8c7 100644 --- a/openrtb_ext/convert_down.go +++ b/openrtb_ext/convert_down.go @@ -31,13 +31,6 @@ func ConvertDownTo25(r *RequestWrapper) error { } } - // Remove fields introduced in OpenRTB 2.6+. The previous OpenRTB 2.5 spec did not specify that - // bidders must tolerate new or unexpected fields. - clear26Fields(r) - clear202211Fields(r) - clear202303Fields(r) - clear202309Fields(r) - return nil } diff --git a/openrtb_ext/convert_down_test.go b/openrtb_ext/convert_down_test.go index 1bf112dcb3a..346ad4816ed 100644 --- a/openrtb_ext/convert_down_test.go +++ b/openrtb_ext/convert_down_test.go @@ -35,50 +35,6 @@ func TestConvertDownTo25(t *testing.T) { User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}]}`)}, }, }, - { - name: "2.6-dropped", // integration with clear26Fields - givenRequest: openrtb2.BidRequest{ - ID: "anyID", - CatTax: adcom1.CatTaxIABContent10, - Device: &openrtb2.Device{LangB: "anyLang"}, - }, - expectedRequest: openrtb2.BidRequest{ - ID: "anyID", - Device: &openrtb2.Device{}, - }, - }, - { - name: "2.6-202211-dropped", // integration with clear202211Fields - givenRequest: openrtb2.BidRequest{ - ID: "anyID", - App: &openrtb2.App{InventoryPartnerDomain: "anyDomain"}, - }, - expectedRequest: openrtb2.BidRequest{ - ID: "anyID", - App: &openrtb2.App{}, - }, - }, - { - name: "2.6-202303-dropped", // integration with clear202303Fields - givenRequest: openrtb2.BidRequest{ - ID: "anyID", - Imp: []openrtb2.Imp{{ID: "1", Refresh: &openrtb2.Refresh{Count: ptrutil.ToPtr(1)}}}, - }, - expectedRequest: openrtb2.BidRequest{ - ID: "anyID", - Imp: []openrtb2.Imp{{ID: "1"}}, - }, - }, - { - name: "2.6-202309-dropped", // integration with clear202309Fields - givenRequest: openrtb2.BidRequest{ - ID: "anyID", - ACat: []string{"anyACat"}, - }, - expectedRequest: openrtb2.BidRequest{ - ID: "anyID", - }, - }, { name: "2.6-to-2.5-OtherExtFields", givenRequest: openrtb2.BidRequest{ diff --git a/openrtb_ext/imp_adtonos.go b/openrtb_ext/imp_adtonos.go new file mode 100644 index 00000000000..f59ee35b329 --- /dev/null +++ b/openrtb_ext/imp_adtonos.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ImpExtAdTonos struct { + SupplierID string `json:"supplierId"` +} diff --git a/openrtb_ext/imp_bidmatic.go b/openrtb_ext/imp_bidmatic.go new file mode 100644 index 00000000000..935c977e7ac --- /dev/null +++ b/openrtb_ext/imp_bidmatic.go @@ -0,0 +1,11 @@ +package openrtb_ext + +import "encoding/json" + +// ExtImpBidmatic defines the contract for bidrequest.imp[i].ext.prebid.bidder.bidmatic +type ExtImpBidmatic struct { + SourceId json.Number `json:"source"` + PlacementId int `json:"placementId,omitempty"` + SiteId int `json:"siteId,omitempty"` + BidFloor float64 `json:"bidFloor,omitempty"` +} diff --git a/openrtb_ext/imp_connectad.go b/openrtb_ext/imp_connectad.go index c4c7ab696f2..d530534cf4f 100644 --- a/openrtb_ext/imp_connectad.go +++ b/openrtb_ext/imp_connectad.go @@ -1,7 +1,9 @@ package openrtb_ext +import "github.com/prebid/prebid-server/v2/util/jsonutil" + type ExtImpConnectAd struct { - NetworkID int `json:"networkId"` - SiteID int `json:"siteId"` - Bidfloor float64 `json:"bidfloor,omitempty"` + NetworkID jsonutil.StringInt `json:"networkId"` + SiteID jsonutil.StringInt `json:"siteId"` + Bidfloor float64 `json:"bidfloor,omitempty"` } diff --git a/openrtb_ext/imp_copper6ssp.go b/openrtb_ext/imp_copper6ssp.go new file mode 100644 index 00000000000..a9fd47a1eb6 --- /dev/null +++ b/openrtb_ext/imp_copper6ssp.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtCopper6ssp struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/openrtb_ext/imp_escalax.go b/openrtb_ext/imp_escalax.go new file mode 100644 index 00000000000..15292b59552 --- /dev/null +++ b/openrtb_ext/imp_escalax.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtEscalax struct { + AccountID string `json:"accountId"` + SourceID string `json:"sourceId"` +} diff --git a/openrtb_ext/imp_melozen.go b/openrtb_ext/imp_melozen.go new file mode 100644 index 00000000000..598df6a28e9 --- /dev/null +++ b/openrtb_ext/imp_melozen.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ImpExtMeloZen struct { + PubId string `json:"pubId"` +} diff --git a/openrtb_ext/imp_missena.go b/openrtb_ext/imp_missena.go new file mode 100644 index 00000000000..3e341957123 --- /dev/null +++ b/openrtb_ext/imp_missena.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpMissena struct { + ApiKey string `json:"apiKey"` + Placement string `json:"placement"` + TestMode string `json:"test"` +} diff --git a/openrtb_ext/imp_oraki.go b/openrtb_ext/imp_oraki.go new file mode 100644 index 00000000000..a9dea04434f --- /dev/null +++ b/openrtb_ext/imp_oraki.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtOraki struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/openrtb_ext/imp_pubrise.go b/openrtb_ext/imp_pubrise.go new file mode 100644 index 00000000000..c2b30391748 --- /dev/null +++ b/openrtb_ext/imp_pubrise.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtPubrise struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index d7ff5acc021..58527528ec6 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -3,12 +3,12 @@ package openrtb_ext import ( "encoding/json" "errors" + "maps" + "slices" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/util/jsonutil" - "github.com/prebid/prebid-server/v2/util/maputil" "github.com/prebid/prebid-server/v2/util/ptrutil" - "github.com/prebid/prebid-server/v2/util/sliceutil" ) // RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they @@ -60,6 +60,8 @@ const ( dataKey = "data" schainKey = "schain" us_privacyKey = "us_privacy" + cdepKey = "cdep" + gpcKey = "gpc" ) // LenImp returns the number of impressions without causing the creation of ImpWrapper objects. @@ -93,6 +95,12 @@ func (rw *RequestWrapper) GetImp() []*ImpWrapper { func (rw *RequestWrapper) SetImp(imps []*ImpWrapper) { rw.impWrappers = imps + imparr := make([]openrtb2.Imp, len(imps)) + for i, iw := range imps { + imparr[i] = *iw.Imp + iw.Imp = &imparr[i] + } + rw.Imp = imparr rw.impWrappersAccessed = true } @@ -237,6 +245,7 @@ func (rw *RequestWrapper) rebuildImp() error { return err } rw.Imp[i] = *rw.impWrappers[i].Imp + rw.impWrappers[i].Imp = &rw.Imp[i] } return nil @@ -390,6 +399,8 @@ func (rw *RequestWrapper) rebuildSourceExt() error { return nil } +// Clone clones the request wrapper exts and the imp wrappers +// the cloned imp wrappers are pointing to the bid request imps func (rw *RequestWrapper) Clone() *RequestWrapper { if rw == nil { return nil @@ -412,6 +423,26 @@ func (rw *RequestWrapper) Clone() *RequestWrapper { return &clone } +func (rw *RequestWrapper) CloneAndClearImpWrappers() *RequestWrapper { + if rw == nil { + return nil + } + rw.impWrappersAccessed = false + + clone := *rw + clone.impWrappers = nil + clone.userExt = rw.userExt.Clone() + clone.deviceExt = rw.deviceExt.Clone() + clone.requestExt = rw.requestExt.Clone() + clone.appExt = rw.appExt.Clone() + clone.regExt = rw.regExt.Clone() + clone.siteExt = rw.siteExt.Clone() + clone.doohExt = rw.doohExt.Clone() + clone.sourceExt = rw.sourceExt.Clone() + + return &clone +} + // --------------------------------------------------------------- // UserExt provides an interface for request.user.ext // --------------------------------------------------------------- @@ -648,7 +679,6 @@ func (ue *UserExt) SetConsentedProvidersSettingsOut(cpSettings *ConsentedProvide ue.consentedProvidersSettingsOut = cpSettings ue.consentedProvidersSettingsOutDirty = true - return } func (ue *UserExt) GetPrebid() *ExtUserPrebid { @@ -682,7 +712,7 @@ func (ue *UserExt) Clone() *UserExt { return nil } clone := *ue - clone.ext = maputil.Clone(ue.ext) + clone.ext = maps.Clone(ue.ext) if ue.consent != nil { clonedConsent := *ue.consent @@ -691,14 +721,14 @@ func (ue *UserExt) Clone() *UserExt { if ue.prebid != nil { clone.prebid = &ExtUserPrebid{} - clone.prebid.BuyerUIDs = maputil.Clone(ue.prebid.BuyerUIDs) + clone.prebid.BuyerUIDs = maps.Clone(ue.prebid.BuyerUIDs) } if ue.eids != nil { clonedEids := make([]openrtb2.EID, len(*ue.eids)) for i, eid := range *ue.eids { newEid := eid - newEid.UIDs = sliceutil.Clone(eid.UIDs) + newEid.UIDs = slices.Clone(eid.UIDs) clonedEids[i] = newEid } clone.eids = &clonedEids @@ -708,7 +738,7 @@ func (ue *UserExt) Clone() *UserExt { clone.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{ConsentedProvidersString: ue.consentedProvidersSettingsIn.ConsentedProvidersString} } if ue.consentedProvidersSettingsOut != nil { - clone.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{ConsentedProvidersList: sliceutil.Clone(ue.consentedProvidersSettingsOut.ConsentedProvidersList)} + clone.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{ConsentedProvidersList: slices.Clone(ue.consentedProvidersSettingsOut.ConsentedProvidersList)} } return &clone @@ -859,7 +889,7 @@ func (re *RequestExt) Clone() *RequestExt { } clone := *re - clone.ext = maputil.Clone(re.ext) + clone.ext = maps.Clone(re.ext) if re.prebid != nil { clone.prebid = re.prebid.Clone() @@ -883,6 +913,8 @@ type DeviceExt struct { extDirty bool prebid *ExtDevicePrebid prebidDirty bool + cdep string + cdepDirty bool } func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { @@ -910,6 +942,13 @@ func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { } } + cdepJson, hasCDep := de.ext[cdepKey] + if hasCDep && cdepJson != nil { + if err := jsonutil.Unmarshal(cdepJson, &de.cdep); err != nil { + return err + } + } + return nil } @@ -931,6 +970,19 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { de.prebidDirty = false } + if de.cdepDirty { + if len(de.cdep) > 0 { + rawjson, err := jsonutil.Marshal(de.cdep) + if err != nil { + return nil, err + } + de.ext[cdepKey] = rawjson + } else { + delete(de.ext, cdepKey) + } + de.cdepDirty = false + } + de.extDirty = false if len(de.ext) == 0 { return nil, nil @@ -939,7 +991,7 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { } func (de *DeviceExt) Dirty() bool { - return de.extDirty || de.prebidDirty + return de.extDirty || de.prebidDirty || de.cdepDirty } func (de *DeviceExt) GetExt() map[string]json.RawMessage { @@ -968,13 +1020,22 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) { de.prebidDirty = true } +func (de *DeviceExt) GetCDep() string { + return de.cdep +} + +func (de *DeviceExt) SetCDep(cdep string) { + de.cdep = cdep + de.cdepDirty = true +} + func (de *DeviceExt) Clone() *DeviceExt { if de == nil { return nil } clone := *de - clone.ext = maputil.Clone(de.ext) + clone.ext = maps.Clone(de.ext) if de.prebid != nil { clonedPrebid := *de.prebid @@ -1088,7 +1149,7 @@ func (ae *AppExt) Clone() *AppExt { } clone := *ae - clone.ext = maputil.Clone(ae.ext) + clone.ext = maps.Clone(ae.ext) clone.prebid = ptrutil.Clone(ae.prebid) @@ -1154,7 +1215,7 @@ func (de *DOOHExt) Clone() *DOOHExt { } clone := *de - clone.ext = maputil.Clone(de.ext) + clone.ext = maps.Clone(de.ext) return &clone } @@ -1170,6 +1231,8 @@ type RegExt struct { dsaDirty bool gdpr *int8 gdprDirty bool + gpc *string + gpcDirty bool usPrivacy string usPrivacyDirty bool } @@ -1213,6 +1276,13 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error { } } + gpcJson, hasGPC := re.ext[gpcKey] + if hasGPC && gpcJson != nil { + if err := jsonutil.Unmarshal(gpcJson, &re.gpc); err != nil { + return err + } + } + return nil } @@ -1256,6 +1326,19 @@ func (re *RegExt) marshal() (json.RawMessage, error) { re.usPrivacyDirty = false } + if re.gpcDirty { + if re.gpc != nil { + rawjson, err := jsonutil.Marshal(re.gpc) + if err != nil { + return nil, err + } + re.ext[gpcKey] = rawjson + } else { + delete(re.ext, gpcKey) + } + re.gpcDirty = false + } + re.extDirty = false if len(re.ext) == 0 { return nil, nil @@ -1264,7 +1347,7 @@ func (re *RegExt) marshal() (json.RawMessage, error) { } func (re *RegExt) Dirty() bool { - return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty + return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty || re.gpcDirty } func (re *RegExt) GetExt() map[string]json.RawMessage { @@ -1306,6 +1389,19 @@ func (re *RegExt) SetGDPR(gdpr *int8) { re.gdprDirty = true } +func (re *RegExt) GetGPC() *string { + if re.gpc == nil { + return nil + } + gpc := *re.gpc + return &gpc +} + +func (re *RegExt) SetGPC(gpc *string) { + re.gpc = gpc + re.gpcDirty = true +} + func (re *RegExt) GetUSPrivacy() string { uSPrivacy := re.usPrivacy return uSPrivacy @@ -1322,7 +1418,7 @@ func (re *RegExt) Clone() *RegExt { } clone := *re - clone.ext = maputil.Clone(re.ext) + clone.ext = maps.Clone(re.ext) clone.gdpr = ptrutil.Clone(re.gdpr) @@ -1418,7 +1514,7 @@ func (se *SiteExt) Clone() *SiteExt { } clone := *se - clone.ext = maputil.Clone(se.ext) + clone.ext = maps.Clone(se.ext) clone.amp = ptrutil.Clone(se.amp) return &clone @@ -1521,7 +1617,7 @@ func (se *SourceExt) Clone() *SourceExt { } clone := *se - clone.ext = maputil.Clone(se.ext) + clone.ext = maps.Clone(se.ext) clone.schain = cloneSupplyChain(se.schain) @@ -1760,7 +1856,7 @@ func (e *ImpExt) Clone() *ImpExt { } clone := *e - clone.ext = maputil.Clone(e.ext) + clone.ext = maps.Clone(e.ext) if e.prebid != nil { clonedPrebid := *e.prebid @@ -1774,7 +1870,7 @@ func (e *ImpExt) Clone() *ImpExt { } } clonedPrebid.IsRewardedInventory = ptrutil.Clone(e.prebid.IsRewardedInventory) - clonedPrebid.Bidder = maputil.Clone(e.prebid.Bidder) + clonedPrebid.Bidder = maps.Clone(e.prebid.Bidder) clonedPrebid.Options = ptrutil.Clone(e.prebid.Options) clonedPrebid.Floors = ptrutil.Clone(e.prebid.Floors) clone.prebid = &clonedPrebid diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index f04a51a4bdc..d2f1ff48ca9 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -198,6 +198,7 @@ func TestRebuildImp(t *testing.T) { request openrtb2.BidRequest requestImpWrapper []*ImpWrapper expectedRequest openrtb2.BidRequest + expectedAccessed bool expectedError string }{ { @@ -217,11 +218,13 @@ func TestRebuildImp(t *testing.T) { request: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1"}}}, requestImpWrapper: []*ImpWrapper{{Imp: &openrtb2.Imp{ID: "2"}, impExt: &ImpExt{prebid: prebid, prebidDirty: true}}}, expectedRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "2", Ext: prebidJson}}}, + expectedAccessed: true, }, { description: "One - Accessed - Error", request: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1"}}}, requestImpWrapper: []*ImpWrapper{{Imp: nil, impExt: &ImpExt{}}}, + expectedAccessed: true, expectedError: "ImpWrapper RebuildImp called on a nil Imp", }, { @@ -229,6 +232,7 @@ func TestRebuildImp(t *testing.T) { request: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1"}, {ID: "2"}}}, requestImpWrapper: []*ImpWrapper{{Imp: &openrtb2.Imp{ID: "1"}, impExt: &ImpExt{}}, {Imp: &openrtb2.Imp{ID: "2"}, impExt: &ImpExt{prebid: prebid, prebidDirty: true}}}, expectedRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1"}, {ID: "2", Ext: prebidJson}}}, + expectedAccessed: true, }, } @@ -247,6 +251,20 @@ func TestRebuildImp(t *testing.T) { assert.NoError(t, err, test.description) assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description) } + + if test.expectedAccessed && test.expectedError == "" { + bidRequestImps := make(map[string]*openrtb2.Imp, 0) + for i, v := range w.Imp { + bidRequestImps[v.ID] = &w.Imp[i] + } + wrapperImps := make(map[string]*openrtb2.Imp, 0) + for i, v := range w.impWrappers { + wrapperImps[v.ID] = w.impWrappers[i].Imp + } + for k := range bidRequestImps { + assert.Same(t, bidRequestImps[k], wrapperImps[k], test.description) + } + } } } @@ -1856,6 +1874,37 @@ func TestImpWrapperGetImpExt(t *testing.T) { } } +func TestImpWrapperSetImp(t *testing.T) { + origImps := []openrtb2.Imp{ + {ID: "imp1", TagID: "tag1"}, + {ID: "imp2", TagID: "tag2"}, + {ID: "imp3", TagID: "tag3"}, + } + expectedImps := []openrtb2.Imp{ + {ID: "imp1", TagID: "tag4", BidFloor: 0.5}, + {ID: "imp1.1", TagID: "tag2", BidFloor: 0.6}, + {ID: "imp2", TagID: "notag"}, + {ID: "imp3", TagID: "tag3"}, + } + rw := RequestWrapper{BidRequest: &openrtb2.BidRequest{Imp: origImps}} + iw := rw.GetImp() + rw.Imp[0].TagID = "tag4" + rw.Imp[0].BidFloor = 0.5 + iw[1] = &ImpWrapper{Imp: &expectedImps[1]} + *iw[2] = ImpWrapper{Imp: &expectedImps[2]} + iw = append(iw, &ImpWrapper{Imp: &expectedImps[3]}) + + rw.SetImp(iw) + assert.Equal(t, expectedImps, rw.BidRequest.Imp) + iw = rw.GetImp() + // Ensure that the wrapper pointers are in sync. + for i := range rw.BidRequest.Imp { + // Assert the pointers are in sync. + assert.Same(t, &rw.Imp[i], iw[i].Imp) + } + +} + func TestImpExtTid(t *testing.T) { impExt := &ImpExt{} @@ -2164,6 +2213,30 @@ func TestRebuildRegExt(t *testing.T) { regExt: RegExt{usPrivacy: "", usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, }, + { + name: "req_regs_gpc_populated_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_different-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: &strB, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"b"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_same_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: &strA, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_nil_-_cleared", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: nil, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, } for _, tt := range tests { @@ -2184,6 +2257,7 @@ func TestRegExtUnmarshal(t *testing.T) { extJson json.RawMessage expectDSA *ExtRegsDSA expectGDPR *int8 + expectGPC *string expectUSPrivacy string expectError bool }{ @@ -2243,6 +2317,21 @@ func TestRegExtUnmarshal(t *testing.T) { expectGDPR: ptrutil.ToPtr[int8](0), expectError: true, }, + // GPC + { + name: "valid_gpc_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gpc":"some_value"}`), + expectGPC: ptrutil.ToPtr("some_value"), + expectError: false, + }, + { + name: "malformed_gpc_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gpc":nill}`), + expectGPC: nil, + expectError: true, + }, // us_privacy { name: "valid_usprivacy_json", @@ -2338,3 +2427,18 @@ func TestRegExtGetGDPRSetGDPR(t *testing.T) { assert.Equal(t, regExtGDPR, gdpr) assert.NotSame(t, regExtGDPR, gdpr) } + +func TestRegExtGetGPCSetGPC(t *testing.T) { + regExt := &RegExt{} + regExtGPC := regExt.GetGPC() + assert.Nil(t, regExtGPC) + assert.False(t, regExt.Dirty()) + + gpc := ptrutil.ToPtr("Gpc") + regExt.SetGPC(gpc) + assert.True(t, regExt.Dirty()) + + regExtGPC = regExt.GetGPC() + assert.Equal(t, regExtGPC, gpc) + assert.NotSame(t, regExtGPC, gpc) +} diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index d9baea3f4da..449ff939bf5 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -132,14 +132,14 @@ type NonBidExt struct { // NonBid represnts the Non Bid Reason (statusCode) for given impression ID type NonBid struct { - ImpId string `json:"impid"` - StatusCode int `json:"statuscode"` - Ext NonBidExt `json:"ext"` + ImpId string `json:"impid"` + StatusCode int `json:"statuscode"` + Ext *NonBidExt `json:"ext,omitempty"` } // SeatNonBid is collection of NonBid objects with seat information type SeatNonBid struct { NonBid []NonBid `json:"nonbid"` Seat string `json:"seat"` - Ext json.RawMessage `json:"ext"` + Ext json.RawMessage `json:"ext,omitempty"` } diff --git a/ortb/clone.go b/ortb/clone.go index 3023169bc8c..17b82bc84f4 100644 --- a/ortb/clone.go +++ b/ortb/clone.go @@ -421,3 +421,19 @@ func CloneBidRequestPartial(s *openrtb2.BidRequest) *openrtb2.BidRequest { return &c } + +func CloneRegs(s *openrtb2.Regs) *openrtb2.Regs { + if s == nil { + return nil + } + + // Shallow Copy (Value Fields) + c := *s + + // Deep Copy (Pointers) + c.GDPR = ptrutil.Clone(s.GDPR) + c.GPPSID = slices.Clone(s.GPPSID) + c.Ext = slices.Clone(s.Ext) + + return &c +} diff --git a/ortb/clone_test.go b/ortb/clone_test.go index 73d03614db4..f1c0cbb0087 100644 --- a/ortb/clone_test.go +++ b/ortb/clone_test.go @@ -1172,3 +1172,44 @@ func discoverPointerFields(t reflect.Type) []string { } return fields } + +func TestCloneRegs(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneRegs(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.Regs{} + result := CloneRegs(given) + assert.Empty(t, result) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.Regs{ + COPPA: 1, + GDPR: ptrutil.ToPtr(int8(0)), + USPrivacy: "1YNN", + GPP: "SomeGPPStrig", + GPPSID: []int8{1, 2, 3}, + Ext: json.RawMessage(`{"anyField":1}`), + } + result := CloneRegs(given) + assert.Equal(t, given, result, "equality") + assert.NotSame(t, given, result, "pointer") + assert.NotSame(t, given.GDPR, result.GDPR, "gdpr") + assert.NotSame(t, given.GPPSID, result.GPPSID, "gppsid[]") + assert.NotSame(t, given.GPPSID[0], result.GPPSID[0], "gppsid[0]") + assert.NotSame(t, given.Ext, result.Ext, "ext") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Regs{})), + []string{ + "GDPR", + "GPPSID", + "Ext", + }) + }) +} diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go index 265d47e8595..3a0934c5722 100644 --- a/privacy/ccpa/consentwriter.go +++ b/privacy/ccpa/consentwriter.go @@ -2,7 +2,6 @@ package ccpa import ( "github.com/prebid/openrtb/v20/openrtb2" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) // ConsentWriter implements the old PolicyWriter interface for CCPA. @@ -16,16 +15,14 @@ func (c ConsentWriter) Write(req *openrtb2.BidRequest) error { if req == nil { return nil } - reqWrap := &openrtb_ext.RequestWrapper{BidRequest: req} // Set consent string in USPrivacy if c.Consent != "" { - if regsExt, err := reqWrap.GetRegExt(); err == nil { - regsExt.SetUSPrivacy(c.Consent) - } else { - return err + if req.Regs == nil { + req.Regs = &openrtb2.Regs{} } + req.Regs.USPrivacy = c.Consent } - return reqWrap.RebuildRequest() + return nil } diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go index a92400dce53..fe3dab248ef 100644 --- a/privacy/ccpa/consentwriter_test.go +++ b/privacy/ccpa/consentwriter_test.go @@ -75,7 +75,9 @@ func TestConsentWriterLegacy(t *testing.T) { description: "Success", request: &openrtb2.BidRequest{}, expected: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + Regs: &openrtb2.Regs{ + USPrivacy: "anyConsent", + }, }, }, { @@ -83,9 +85,12 @@ func TestConsentWriterLegacy(t *testing.T) { request: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)}, }, - expectedError: true, + expectedError: false, expected: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)}, + Regs: &openrtb2.Regs{ + USPrivacy: "anyConsent", + Ext: json.RawMessage(`malformed}`), + }, }, }, } diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index 0b719bf1455..463cf3391d8 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -41,15 +41,8 @@ func ReadFromRequestWrapper(req *openrtb_ext.RequestWrapper, gpp gpplib.GppConta WarningCode: errortypes.InvalidPrivacyConsentWarningCode} } - if consent == "" { - // Read consent from request.regs.ext - regsExt, err := req.GetRegExt() - if err != nil { - return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err) - } - if regsExt != nil { - consent = regsExt.GetUSPrivacy() - } + if consent == "" && req.Regs != nil { + consent = req.Regs.USPrivacy } // Read no sale bidders from request.ext.prebid reqExt, err := req.GetRequestExt() @@ -75,21 +68,19 @@ func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) { // Write mutates an OpenRTB bid request with the CCPA regulatory information. func (p Policy) Write(req *openrtb_ext.RequestWrapper) error { - if req == nil { + if req == nil || req.BidRequest == nil { return nil } - regsExt, err := req.GetRegExt() - if err != nil { - return err - } - reqExt, err := req.GetRequestExt() if err != nil { return err } - regsExt.SetUSPrivacy(p.Consent) + if req.Regs == nil { + req.Regs = &openrtb2.Regs{} + } + req.Regs.USPrivacy = p.Consent setPrebidNoSale(p.NoSaleBidders, reqExt) return nil } diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index 20d9f680ba1..c77b4ddc985 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -23,7 +23,7 @@ func TestReadFromRequestWrapper(t *testing.T) { { description: "Success", request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "ABC"}, Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ @@ -83,26 +83,10 @@ func TestReadFromRequestWrapper(t *testing.T) { NoSaleBidders: []string{"a", "b"}, }, }, - { - description: "Malformed Regs.Ext", - request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed`)}, - Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), - }, - expectedError: true, - }, - { - description: "Invalid Regs.Ext Type", - request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123`)}, - Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), - }, - expectedError: true, - }, { description: "Nil Ext", request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "ABC"}, Ext: nil, }, expectedPolicy: Policy{ @@ -113,7 +97,7 @@ func TestReadFromRequestWrapper(t *testing.T) { { description: "Empty Ext", request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "ABC"}, Ext: json.RawMessage(`{}`), }, expectedPolicy: Policy{ @@ -124,7 +108,7 @@ func TestReadFromRequestWrapper(t *testing.T) { { description: "Missing Ext.Prebid No Sale Value", request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "ABC"}, Ext: json.RawMessage(`{"anythingElse":"42"}`), }, expectedPolicy: Policy{ @@ -148,15 +132,6 @@ func TestReadFromRequestWrapper(t *testing.T) { }, expectedError: true, }, - { - description: "Injection Attack", - request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`)}, - }, - expectedPolicy: Policy{ - Consent: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", - }, - }, { description: "GPP Success", request: &openrtb2.BidRequest{ @@ -244,7 +219,7 @@ func TestReadFromRequest(t *testing.T) { { description: "Success", request: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "ABC"}, Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ @@ -353,7 +328,7 @@ func TestWrite(t *testing.T) { policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, request: &openrtb2.BidRequest{}, expected: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + Regs: &openrtb2.Regs{USPrivacy: "anyConsent"}, Ext: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, }, diff --git a/privacy/gdpr/consentwriter.go b/privacy/gdpr/consentwriter.go index 243a6cf79e9..8269352355d 100644 --- a/privacy/gdpr/consentwriter.go +++ b/privacy/gdpr/consentwriter.go @@ -2,13 +2,12 @@ package gdpr import ( "github.com/prebid/openrtb/v20/openrtb2" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) // ConsentWriter implements the PolicyWriter interface for GDPR TCF. type ConsentWriter struct { - Consent string - RegExtGDPR *int8 + Consent string + GDPR *int8 } // Write mutates an OpenRTB bid request with the GDPR TCF consent. @@ -16,26 +15,19 @@ func (c ConsentWriter) Write(req *openrtb2.BidRequest) error { if req == nil { return nil } - reqWrap := &openrtb_ext.RequestWrapper{BidRequest: req} - if c.RegExtGDPR != nil { - if regsExt, err := reqWrap.GetRegExt(); err == nil { - regsExt.SetGDPR(c.RegExtGDPR) - } else { - return err + if c.GDPR != nil { + if req.Regs == nil { + req.Regs = &openrtb2.Regs{} } + req.Regs.GDPR = c.GDPR } if c.Consent != "" { - if userExt, err := reqWrap.GetUserExt(); err == nil { - userExt.SetConsent(&c.Consent) - } else { - return err + if req.User == nil { + req.User = &openrtb2.User{} } - } - - if err := reqWrap.RebuildRequest(); err != nil { - return err + req.User.Consent = c.Consent } return nil diff --git a/privacy/gdpr/consentwriter_test.go b/privacy/gdpr/consentwriter_test.go index 47f24bc9ecc..436f46dd563 100644 --- a/privacy/gdpr/consentwriter_test.go +++ b/privacy/gdpr/consentwriter_test.go @@ -27,14 +27,14 @@ func TestConsentWriter(t *testing.T) { consent: "anyConsent", request: &openrtb2.BidRequest{}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + Consent: "anyConsent"}}, }, { description: "Enabled With Nil Request User Ext Object", consent: "anyConsent", request: &openrtb2.BidRequest{User: &openrtb2.User{}}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + Consent: "anyConsent"}}, }, { description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite", @@ -42,29 +42,25 @@ func TestConsentWriter(t *testing.T) { request: &openrtb2.BidRequest{User: &openrtb2.User{ Ext: json.RawMessage(`{"existing":"any"}`)}}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, + Consent: "anyConsent", + Ext: json.RawMessage(`{"existing":"any"}`)}}, }, { description: "Enabled With Existing Request User Ext Object - Overwrites", consent: "anyConsent", request: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, + Consent: "toBeOverwritten", + Ext: json.RawMessage(`{"existing":"any"}`)}}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, - }, - { - description: "Enabled With Existing Malformed Request User Ext Object", - consent: "anyConsent", - request: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`malformed`)}}, - expectedError: true, + Consent: "anyConsent", + Ext: json.RawMessage(`{"existing":"any"}`)}}, }, { description: "Injection Attack With Nil Request User Object", consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", request: &openrtb2.BidRequest{}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", }}, }, { @@ -72,7 +68,8 @@ func TestConsentWriter(t *testing.T) { consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", request: &openrtb2.BidRequest{User: &openrtb2.User{}}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", + Ext: nil, }}, }, { @@ -82,7 +79,8 @@ func TestConsentWriter(t *testing.T) { Ext: json.RawMessage(`{"existing":"any"}`), }}, expected: &openrtb2.BidRequest{User: &openrtb2.User{ - Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"","existing":"any"}`), + Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", + Ext: json.RawMessage(`{"existing":"any"}`), }}, }, } diff --git a/sample/001_banner/pbjs.html b/sample/001_banner/pbjs.html new file mode 100644 index 00000000000..af6ad643eec --- /dev/null +++ b/sample/001_banner/pbjs.html @@ -0,0 +1,121 @@ + + + + + + + + + + + +

001_banner

+

+ This demo uses Prebid.js to interact with Prebid Server to fill the ad slot test-div-1 + The auction request to Prebid Server uses a stored request, which in turn links to a stored response.
+ Look for the /auction request in your browser's developer tool to inspect the request + and response. +

+

↓I am ad unit test-div-1 ↓

+
+
+ + diff --git a/schain/schainwriter.go b/schain/schainwriter.go index b7ff0b52e95..141987ff4c5 100644 --- a/schain/schainwriter.go +++ b/schain/schainwriter.go @@ -3,7 +3,6 @@ package schain import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/openrtb_ext" - "github.com/prebid/prebid-server/v2/util/jsonutil" ) // NewSChainWriter creates an ORTB 2.5 schain writer instance @@ -34,9 +33,9 @@ type SChainWriter struct { // Write selects an schain from the multi-schain ORTB 2.5 location (req.ext.prebid.schains) for the specified bidder // and copies it to the ORTB 2.5 location (req.source.ext). If no schain exists for the bidder in the multi-schain // location and no wildcard schain exists, the request is not modified. -func (w SChainWriter) Write(req *openrtb2.BidRequest, bidder string) { +func (w SChainWriter) Write(reqWrapper *openrtb_ext.RequestWrapper, bidder string) { const sChainWildCard = "*" - var selectedSChain *openrtb2.SupplyChain + var selectedSChain openrtb2.SupplyChain wildCardSChain := w.sChainsByBidder[sChainWildCard] bidderSChain := w.sChainsByBidder[bidder] @@ -46,32 +45,27 @@ func (w SChainWriter) Write(req *openrtb2.BidRequest, bidder string) { return } - selectedSChain = &openrtb2.SupplyChain{Ver: "1.0"} + selectedSChain = openrtb2.SupplyChain{Ver: "1.0"} if bidderSChain != nil { - selectedSChain = bidderSChain + selectedSChain = *bidderSChain } else if wildCardSChain != nil { - selectedSChain = wildCardSChain + selectedSChain = *wildCardSChain } - schain := openrtb_ext.ExtRequestPrebidSChain{ - SChain: *selectedSChain, - } - - if req.Source == nil { - req.Source = &openrtb2.Source{} + if reqWrapper.Source == nil { + reqWrapper.Source = &openrtb2.Source{} } else { - sourceCopy := *req.Source - req.Source = &sourceCopy + // Copy Source to avoid shared memory issues. + // Source may be modified differently for different bidders in request + sourceCopy := *reqWrapper.Source + reqWrapper.Source = &sourceCopy } - if w.hostSChainNode != nil { - schain.SChain.Nodes = append(schain.SChain.Nodes, *w.hostSChainNode) - } + reqWrapper.Source.SChain = &selectedSChain - sourceExt, err := jsonutil.Marshal(schain) - if err == nil { - req.Source.Ext = sourceExt + if w.hostSChainNode != nil { + reqWrapper.Source.SChain.Nodes = append(reqWrapper.Source.SChain.Nodes, *w.hostSChainNode) } } diff --git a/schain/schainwriter_test.go b/schain/schainwriter_test.go index d9b1358ffe6..757fc616052 100644 --- a/schain/schainwriter_test.go +++ b/schain/schainwriter_test.go @@ -17,206 +17,348 @@ func TestSChainWriter(t *testing.T) { const seller2SChain string = `"schain":{"complete":2,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":2}],"ver":"2.0"}` const seller3SChain string = `"schain":{"complete":3,"nodes":[{"asi":"directseller3.com","sid":"00003","rid":"BidRequest3","hp":3}],"ver":"3.0"}` const sellerWildCardSChain string = `"schain":{"complete":1,"nodes":[{"asi":"wildcard1.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}` - const hostNode string = `{"asi":"pbshostcompany.com","sid":"00001","rid":"BidRequest","hp":1}` const seller1Node string = `{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}` tests := []struct { description string - giveRequest openrtb2.BidRequest + giveRequest *openrtb_ext.RequestWrapper giveBidder string giveHostSChain *openrtb2.SupplyChainNode - wantRequest openrtb2.BidRequest + wantRequest *openrtb_ext.RequestWrapper wantError bool }{ { description: "nil source, nil ext.prebid.schains and empty host schain", - giveRequest: openrtb2.BidRequest{ - Ext: nil, - Source: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: nil, + Source: nil, + }, }, + giveBidder: "appnexus", giveHostSChain: nil, - wantRequest: openrtb2.BidRequest{ - Ext: nil, - Source: nil, + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: nil, + Source: nil, + }, }, }, { - description: "Use source schain -- no bidder schain or wildcard schain in nil ext.prebid.schains", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller2SChain + `}`), + description: "Use source schain -- no bidder schain or wildcard schain in nil ext.prebid.schains - so source.schain is set and unmodified", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Ver: "1.1", + }, + }, }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller2SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Ver: "1.1", + }, + }, }, }, }, { - description: "Use source schain -- no bidder schain or wildcard schain in not nil ext.prebid.schains", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller2SChain + `}`), + description: "Use source schain -- no bidder schain or wildcard schain in not nil ext.prebid.schains - so source.schain is set and unmodified", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Ver: "1.1", + }, + Ext: json.RawMessage(`{"some":"data"}`), + }, }, }, giveBidder: "rubicon", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller2SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Ver: "1.1", + }, + Ext: json.RawMessage(`{"some":"data"}`), + }, }, }, }, { - description: "Use schain for bidder in ext.prebid.schains; ensure other ext.source field values are retained.", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - FD: openrtb2.Int8Ptr(1), - TID: "tid data", - PChain: "pchain data", - Ext: json.RawMessage(`{` + seller2SChain + `}`), + description: "Use schain for bidder in ext.prebid.schains; ensure other source field values are retained.", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + FD: openrtb2.Int8Ptr(1), + TID: "tid data", + PChain: "pchain data", + Ext: json.RawMessage(`{"some":"data"}`), + }, }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - FD: openrtb2.Int8Ptr(1), - TID: "tid data", - PChain: "pchain data", - Ext: json.RawMessage(`{` + seller1SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + FD: openrtb2.Int8Ptr(1), + TID: "tid data", + PChain: "pchain data", + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: json.RawMessage(`{"some":"data"}`), + }, }, }, }, { description: "Use schain for bidder in ext.prebid.schains, nil req.source ", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: nil, + }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller1SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, }, }, }, { description: "Use wildcard schain in ext.prebid.schains.", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: nil, + }, }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + sellerWildCardSChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "wildcard1.com", + SID: "wildcard1", + RID: "WildcardReq1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, }, }, }, { description: "Use schain for bidder in ext.prebid.schains instead of wildcard.", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: nil, + }, }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller1SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["*"],` + sellerWildCardSChain + `}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, }, }, }, { description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains.", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller3SChain + `}`), + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller3SChain + `}`), + }, }, }, giveBidder: "appnexus", - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{` + seller3SChain + `}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],` + seller1SChain + `},{"bidders":["appnexus"],` + seller2SChain + `}]}}`), + Source: &openrtb2.Source{ + Ext: json.RawMessage(`{` + seller3SChain + `}`), + }, }, }, wantError: true, }, { description: "Schain in request, host schain defined, source.ext for bidder request should update with appended host schain", - giveRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["testbidder"],"schain":{"complete":1,"nodes":[` + seller1Node + `],"ver":"1.0"}}]}}`), - Source: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["testbidder"],"schain":{"complete":1,"nodes":[` + seller1Node + `],"ver":"1.0"}}]}}`), + Source: nil, + }, }, giveBidder: "testbidder", giveHostSChain: &openrtb2.SupplyChainNode{ ASI: "pbshostcompany.com", SID: "00001", RID: "BidRequest", HP: openrtb2.Int8Ptr(1), }, - wantRequest: openrtb2.BidRequest{ - Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["testbidder"],"schain":{"complete":1,"nodes":[` + seller1Node + `],"ver":"1.0"}}]}}`), - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{"schain":{"complete":1,"nodes":[` + seller1Node + `,` + hostNode + `],"ver":"1.0"}}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["testbidder"],"schain":{"complete":1,"nodes":[` + seller1Node + `],"ver":"1.0"}}]}}`), + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Complete: 1, + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "directseller1.com", + SID: "00001", + RID: "BidRequest1", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + { + ASI: "pbshostcompany.com", + SID: "00001", + RID: "BidRequest", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, }, }, }, { description: "No Schain in request, host schain defined, source.ext for bidder request should have just the host schain", - giveRequest: openrtb2.BidRequest{ - Ext: nil, - Source: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: nil, + Source: nil, + }, }, giveBidder: "testbidder", giveHostSChain: &openrtb2.SupplyChainNode{ ASI: "pbshostcompany.com", SID: "00001", RID: "BidRequest", HP: openrtb2.Int8Ptr(1), }, - wantRequest: openrtb2.BidRequest{ - Ext: nil, - Source: &openrtb2.Source{ - Ext: json.RawMessage(`{"schain":{"complete":0,"nodes":[` + hostNode + `],"ver":"1.0"}}`), + wantRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: nil, + Source: &openrtb2.Source{ + SChain: &openrtb2.SupplyChain{ + Ver: "1.0", + Ext: nil, + Nodes: []openrtb2.SupplyChainNode{ + { + ASI: "pbshostcompany.com", + SID: "00001", + RID: "BidRequest", + HP: openrtb2.Int8Ptr(1), + Ext: nil, + }, + }, + }, + Ext: nil, + }, }, }, }, } for _, tt := range tests { - // unmarshal ext to get schains object needed to initialize writer - var reqExt *openrtb_ext.ExtRequest - if tt.giveRequest.Ext != nil { - reqExt = &openrtb_ext.ExtRequest{} - err := jsonutil.UnmarshalValid(tt.giveRequest.Ext, reqExt) - if err != nil { - t.Error("Unable to unmarshal request.ext") + t.Run(tt.description, func(t *testing.T) { + // unmarshal ext to get schains object needed to initialize writer + var reqExt *openrtb_ext.ExtRequest + if tt.giveRequest.Ext != nil { + reqExt = &openrtb_ext.ExtRequest{} + err := jsonutil.UnmarshalValid(tt.giveRequest.Ext, reqExt) + if err != nil { + t.Error("Unable to unmarshal request.ext") + } } - } - writer, err := NewSChainWriter(reqExt, tt.giveHostSChain) + writer, err := NewSChainWriter(reqExt, tt.giveHostSChain) - if tt.wantError { - assert.NotNil(t, err) - assert.Nil(t, writer) - } else { - assert.Nil(t, err) - assert.NotNil(t, writer) + if tt.wantError { + assert.NotNil(t, err) + assert.Nil(t, writer) + } else { + assert.Nil(t, err) + assert.NotNil(t, writer) - writer.Write(&tt.giveRequest, tt.giveBidder) + writer.Write(tt.giveRequest, tt.giveBidder) - assert.Equal(t, tt.wantRequest, tt.giveRequest, tt.description) - } + assert.Equal(t, tt.wantRequest, tt.giveRequest, tt.description) + } + }) } } diff --git a/static/bidder-info/adtonos.yaml b/static/bidder-info/adtonos.yaml new file mode 100644 index 00000000000..37a81372710 --- /dev/null +++ b/static/bidder-info/adtonos.yaml @@ -0,0 +1,24 @@ +endpoint: https://exchange.adtonos.com/bid/{{.PublisherID}} +maintainer: + email: support@adtonos.com +gvlVendorID: 682 +geoscope: + - global +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - audio + # NOTE: This is purely an audio ad exchange - video ads are synthesized from audio ads for compatibility + # with mobile games that only understand video mimetypes. The visual layer is just a placeholder. + - video + site: + mediaTypes: + - audio + dooh: + mediaTypes: + - audio +userSync: + redirect: + url: https://play.adtonos.com/redir?to={{.RedirectURL}} + userMacro: '@UUID@' diff --git a/static/bidder-info/bidmatic.yaml b/static/bidder-info/bidmatic.yaml new file mode 100644 index 00000000000..19211190033 --- /dev/null +++ b/static/bidder-info/bidmatic.yaml @@ -0,0 +1,18 @@ +endpoint: "http://adapter.bidmatic.io/pbs/ortb" +maintainer: + email: "advertising@bidmatic.io" +gvlVendorID: 1134 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + # bidmatic supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - iframe diff --git a/static/bidder-info/bluesea.yaml b/static/bidder-info/bluesea.yaml index 14667cafd6e..a9a5ca203d6 100644 --- a/static/bidder-info/bluesea.yaml +++ b/static/bidder-info/bluesea.yaml @@ -3,9 +3,15 @@ maintainer: email: prebid@blueseasx.com endpointCompression: gzip modifyingVastXmlAllowed: true +gvlVendorID: 1294 capabilities: app: mediaTypes: - banner - native - video + site: + mediaTypes: + - banner + - native + - video diff --git a/static/bidder-info/copper6ssp.yaml b/static/bidder-info/copper6ssp.yaml new file mode 100644 index 00000000000..92dc5cfb7e8 --- /dev/null +++ b/static/bidder-info/copper6ssp.yaml @@ -0,0 +1,21 @@ +endpoint: "https://endpoint.copper6.com/" +maintainer: + email: "info@copper6.com" +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://csync.copper6.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" + iframe: + url: "https://csync.copper6.com/pbserverIframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&pbserverUrl={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/escalax.yaml b/static/bidder-info/escalax.yaml new file mode 100644 index 00000000000..8d6871182ce --- /dev/null +++ b/static/bidder-info/escalax.yaml @@ -0,0 +1,18 @@ +endpoint: 'http://bidder_us.escalax.io/?partner={{.SourceId}}&token={{.AccountID}}&type=pbs' +maintainer: + email: 'connect@escalax.io' +geoscope: + - global +endpointCompression: "GZIP" +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native diff --git a/static/bidder-info/freewheelssp.yaml b/static/bidder-info/freewheelssp.yaml index 8c9286cbbc0..cd18c2d8172 100644 --- a/static/bidder-info/freewheelssp.yaml +++ b/static/bidder-info/freewheelssp.yaml @@ -13,4 +13,7 @@ capabilities: userSync: iframe: url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" - userMacro: "{viewerid}" \ No newline at end of file + userMacro: "{viewerid}" +openrtb: + version: 2.6 + gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml index f7e782e40df..945acf7ca5c 100644 --- a/static/bidder-info/gumgum.yaml +++ b/static/bidder-info/gumgum.yaml @@ -12,3 +12,6 @@ userSync: url: "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" userMacro: "" # gumgum appends the user id to end of the redirect url and does not utilize a macro +openrtb: + version: 2.6 + gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/kargo.yaml b/static/bidder-info/kargo.yaml index 1a7a77eb8bb..6acd679ebc0 100644 --- a/static/bidder-info/kargo.yaml +++ b/static/bidder-info/kargo.yaml @@ -15,4 +15,5 @@ userSync: userMacro: "$UID" endpointCompression: "GZIP" openrtb: + version: 2.6 gpp-supported: true diff --git a/static/bidder-info/lemmadigital.yaml b/static/bidder-info/lemmadigital.yaml index 535c91ffa77..03cabc8f710 100644 --- a/static/bidder-info/lemmadigital.yaml +++ b/static/bidder-info/lemmadigital.yaml @@ -1,4 +1,4 @@ -endpoint: "https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{.PublisherID}}&aid={{.AdUnit}}" +endpoint: "https://pbid.lemmamedia.com/lemma/servad?src=prebid&pid={{.PublisherID}}&aid={{.AdUnit}}" maintainer: email: support@lemmatechnologies.com endpointCompression: gzip @@ -11,4 +11,8 @@ capabilities: site: mediaTypes: - banner - - video \ No newline at end of file + - video +userSync: + iframe: + url: "https://sync.lemmadigital.com/setuid?publisher=850&redirect={{.RedirectURL}}" + userMacro: "${UUID}" \ No newline at end of file diff --git a/static/bidder-info/melozen.yaml b/static/bidder-info/melozen.yaml new file mode 100644 index 00000000000..391e0a8d43b --- /dev/null +++ b/static/bidder-info/melozen.yaml @@ -0,0 +1,19 @@ +# We have the following regional endpoint domains: us-east and us-west +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +endpoint: "https://prebid.melozen.com/rtb/v2/bid?publisher_id={{.PublisherID}}" +endpointCompression: gzip +geoscope: + - global +maintainer: + email: DSP@melodong.com +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/missena.yaml b/static/bidder-info/missena.yaml new file mode 100644 index 00000000000..47f089b9c5a --- /dev/null +++ b/static/bidder-info/missena.yaml @@ -0,0 +1,16 @@ +endpoint: https://bid.missena.io/ +maintainer: + email: prebid@missena.com +gvlVendorID: 687 +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner +userSync: + iframe: + url: https://sync.missena.io/iframe?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: $UID \ No newline at end of file diff --git a/static/bidder-info/mobilefuse.yaml b/static/bidder-info/mobilefuse.yaml index 1d6b323c3a6..62714f15124 100644 --- a/static/bidder-info/mobilefuse.yaml +++ b/static/bidder-info/mobilefuse.yaml @@ -13,3 +13,6 @@ capabilities: - video - native endpointCompression: "GZIP" +openrtb: + version: 2.6 + gpp-supported: true diff --git a/static/bidder-info/openx.yaml b/static/bidder-info/openx.yaml index 9837b5dc92c..d001af72eb2 100644 --- a/static/bidder-info/openx.yaml +++ b/static/bidder-info/openx.yaml @@ -19,4 +19,6 @@ userSync: redirect: url: "https://rtb.openx.net/sync/prebid?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r={{.RedirectURL}}" userMacro: "${UID}" +openrtb: + version: 2.6 diff --git a/static/bidder-info/oraki.yaml b/static/bidder-info/oraki.yaml new file mode 100644 index 00000000000..d5e767ac540 --- /dev/null +++ b/static/bidder-info/oraki.yaml @@ -0,0 +1,18 @@ +endpoint: "https://eu1.oraki.io/pserver" +maintainer: + email: "prebid@oraki.io" +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://sync.oraki.io/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/ownadx.yaml b/static/bidder-info/ownadx.yaml index 37567db1144..77120c81f23 100644 --- a/static/bidder-info/ownadx.yaml +++ b/static/bidder-info/ownadx.yaml @@ -1,4 +1,4 @@ -endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}" +endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.SeatID}}/{{.SspID}}?token={{.TokenID}}" maintainer: email: prebid-team@techbravo.com capabilities: diff --git a/static/bidder-info/playdigo.yaml b/static/bidder-info/playdigo.yaml new file mode 100644 index 00000000000..b90b680f183 --- /dev/null +++ b/static/bidder-info/playdigo.yaml @@ -0,0 +1,24 @@ +endpoint: "https://server.playdigo.com/pserver" +geoscope: + - USA +maintainer: + email: "yr@playdigo.com" +gvlVendorID: 1302 +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://cs.playdigo.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" + iframe: + url: "https://cs.playdigo.com/pbserverIframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&pbserverUrl={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/pubrise.yaml b/static/bidder-info/pubrise.yaml new file mode 100644 index 00000000000..fe5e6cd6d40 --- /dev/null +++ b/static/bidder-info/pubrise.yaml @@ -0,0 +1,21 @@ +endpoint: "https://backend.pubrise.ai/" +maintainer: + email: "prebid@pubrise.ai" +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://sync.pubrise.ai/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" + iframe: + url: "https://sync.pubrise.ai/pbserverIframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&pbserverUrl={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/pulsepoint.yaml b/static/bidder-info/pulsepoint.yaml index 762dbbb0c73..87aff0b5f04 100644 --- a/static/bidder-info/pulsepoint.yaml +++ b/static/bidder-info/pulsepoint.yaml @@ -19,3 +19,6 @@ userSync: redirect: url: "https://bh.contextweb.com/rtset?pid=561205&ev=1&rurl={{.RedirectURL}}" userMacro: "%%VGUID%%" +openrtb: + version: 2.6 + gpp-supported: true diff --git a/static/bidder-info/qt.yaml b/static/bidder-info/qt.yaml new file mode 100644 index 00000000000..a8d16e574bd --- /dev/null +++ b/static/bidder-info/qt.yaml @@ -0,0 +1,19 @@ +endpoint: "https://endpoint1.qt.io/pserver" +maintainer: + email: "qtssp-support@qt.io" +gvlVendorID: 1331 +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://cs.qt.io/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-info/rubicon.yaml b/static/bidder-info/rubicon.yaml index c3943058511..b4c2cfce6d2 100644 --- a/static/bidder-info/rubicon.yaml +++ b/static/bidder-info/rubicon.yaml @@ -13,6 +13,9 @@ xapi: maintainer: email: "header-bidding@rubiconproject.com" gvlVendorID: 52 +openrtb: + version: 2.6 + gpp-supported: true capabilities: app: mediaTypes: diff --git a/static/bidder-info/smartx.yaml b/static/bidder-info/smartx.yaml index 9a387ecfbd2..a3c96a57528 100644 --- a/static/bidder-info/smartx.yaml +++ b/static/bidder-info/smartx.yaml @@ -1,4 +1,6 @@ endpoint: "https://bid.smartclip.net/bid/1005" +openrtb: + version: 2.6 maintainer: email: "bidding@smartclip.tv" gvlVendorID: 115 diff --git a/static/bidder-info/sonobi.yaml b/static/bidder-info/sonobi.yaml index 6f9afc36b3f..dcbcbd2fda7 100644 --- a/static/bidder-info/sonobi.yaml +++ b/static/bidder-info/sonobi.yaml @@ -7,11 +7,16 @@ capabilities: mediaTypes: - banner - video + - native app: mediaTypes: - banner - video + - native userSync: + iframe: + url: "https://sync.go.sonobi.com/uc.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&loc={{.RedirectURL}}" + userMacro: "[UID]" redirect: url: "https://sync.go.sonobi.com/us.gif?loc={{.RedirectURL}}" userMacro: "[UID]" diff --git a/static/bidder-info/sovrn.yaml b/static/bidder-info/sovrn.yaml index 62d74152b0b..fad1850d4b3 100644 --- a/static/bidder-info/sovrn.yaml +++ b/static/bidder-info/sovrn.yaml @@ -1,6 +1,7 @@ endpoint: "http://pbs.lijit.com/rtb/bid?src=prebid_server" maintainer: email: "sovrnoss@sovrn.com" +endpointCompression: gzip gvlVendorID: 13 modifyingVastXmlAllowed: true capabilities: diff --git a/static/bidder-info/streamlyn.yaml b/static/bidder-info/streamlyn.yaml new file mode 100644 index 00000000000..0cf1444ef29 --- /dev/null +++ b/static/bidder-info/streamlyn.yaml @@ -0,0 +1,2 @@ +endpoint: "http://rtba.bidsxchange.com/openrtb/{{.PublisherID}}?host={{.Host}}" +aliasOf: "limelightDigital" diff --git a/static/bidder-info/tgm.yaml b/static/bidder-info/tgm.yaml new file mode 100644 index 00000000000..29d2039ee3f --- /dev/null +++ b/static/bidder-info/tgm.yaml @@ -0,0 +1 @@ +aliasOf: "limelightDigital" diff --git a/static/bidder-info/triplelift.yaml b/static/bidder-info/triplelift.yaml index 605bcc71e6e..79a8951680f 100644 --- a/static/bidder-info/triplelift.yaml +++ b/static/bidder-info/triplelift.yaml @@ -12,9 +12,10 @@ capabilities: - banner - video userSync: - # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. - # Contact this bidder directly at the email address above to ask about enabling user sync. - # + # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. + # If you are a publisher hosting your own Prebid Server, contact this bidder directly at the email address above to ask about enabling user sync. + # If you are a Prebid Server Host, please have your publisher contact the bidder. + # iframe: url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: $UID @@ -23,4 +24,5 @@ userSync: userMacro: "$UID" endpointCompression: "GZIP" openrtb: + version: 2.6 gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/triplelift_native.yaml b/static/bidder-info/triplelift_native.yaml index ff93b544c4c..c5e08152692 100644 --- a/static/bidder-info/triplelift_native.yaml +++ b/static/bidder-info/triplelift_native.yaml @@ -12,8 +12,9 @@ capabilities: mediaTypes: - native userSync: - # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. - # Contact this bidder directly at the email address above to ask about enabling user sync. + # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. + # If you are a publisher hosting your own Prebid Server, contact this bidder directly at the email address above to ask about enabling user sync. + # If you are a Prebid Server Host, please have your publisher contact the bidder. # iframe: url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" @@ -22,4 +23,5 @@ userSync: url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: "$UID" openrtb: + version: 2.6 gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/unruly.yaml b/static/bidder-info/unruly.yaml index 6389f831a64..62f03dec84d 100644 --- a/static/bidder-info/unruly.yaml +++ b/static/bidder-info/unruly.yaml @@ -1,7 +1,13 @@ endpoint: "https://targeting.unrulymedia.com/unruly_prebid_server" +endpointCompression: gzip +geoscope: + - global maintainer: email: "prebidsupport@unrulygroup.com" gvlVendorID: 36 +openrtb: + version: 2.6 + gpp-supported: true capabilities: app: mediaTypes: @@ -14,4 +20,4 @@ capabilities: userSync: redirect: url: "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" - userMacro: "[RX_UUID]" \ No newline at end of file + userMacro: "[RX_UUID]" diff --git a/static/bidder-info/vidazoo.yaml b/static/bidder-info/vidazoo.yaml new file mode 100644 index 00000000000..a58f6849501 --- /dev/null +++ b/static/bidder-info/vidazoo.yaml @@ -0,0 +1,20 @@ +endpoint: "https://prebidsrvr.cootlogix.com/openrtb/" +maintainer: + email: "dev@vidazoo.com" +gvlVendorID: 744 +endpointCompression: gzip +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + iframe: + url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}} + userMacro: ${userId} +openrtb: + gpp_supported: true \ No newline at end of file diff --git a/static/bidder-info/yieldmo.yaml b/static/bidder-info/yieldmo.yaml index 02ab8721a16..17b25ecbbf1 100644 --- a/static/bidder-info/yieldmo.yaml +++ b/static/bidder-info/yieldmo.yaml @@ -2,6 +2,8 @@ endpoint: "https://ads.yieldmo.com/exchange/prebid-server" maintainer: email: "prebid@yieldmo.com" gvlVendorID: 173 +openrtb: + version: 2.6 capabilities: app: mediaTypes: diff --git a/static/bidder-params/adtonos.json b/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..e43d23e2b4b --- /dev/null +++ b/static/bidder-params/adtonos.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": ["supplierId"] +} \ No newline at end of file diff --git a/static/bidder-params/bidmatic.json b/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..b3002a55ac5 --- /dev/null +++ b/static/bidder-params/bidmatic.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": ["source"] +} diff --git a/static/bidder-params/copper6ssp.json b/static/bidder-params/copper6ssp.json new file mode 100644 index 00000000000..9467e48e9b9 --- /dev/null +++ b/static/bidder-params/copper6ssp.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Copper6SSPs Adapter Params", + "description": "A schema which validates params accepted by the Copper6SSP adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] +} \ No newline at end of file diff --git a/static/bidder-params/escalax.json b/static/bidder-params/escalax.json new file mode 100644 index 00000000000..3045e7f1898 --- /dev/null +++ b/static/bidder-params/escalax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Escalax Adapter Params", + "description": "A schema which validates params accepted by the Escalax adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} \ No newline at end of file diff --git a/static/bidder-params/improvedigital.json b/static/bidder-params/improvedigital.json index 55412c2f513..7c44faf3bed 100644 --- a/static/bidder-params/improvedigital.json +++ b/static/bidder-params/improvedigital.json @@ -7,16 +7,12 @@ "placementId": { "type": "integer", "minimum": 1, - "description": "An ID which identifies this placement of the impression" + "description": "The placement ID from Improve Digital" }, "publisherId": { "type": "integer", "minimum": 1, - "description": "An ID which identifies publisher. Required when using a placementKey" - }, - "placementKey": { - "type": "string", - "description": "An uniq name which identifies this placement of the impression. Must be used with publisherId" + "description": "The publisher ID from Improve Digital" }, "keyValues": { "type": "object", @@ -32,13 +28,12 @@ "type": "integer" } }, - "required": ["w", "h"], + "required": [ + "w", + "h" + ], "description": "Placement size" } }, - "oneOf": [{ - "required": ["placementId"] - }, { - "required": ["publisherId", "placementKey"] - }] -} + "required": ["placementId"] +} \ No newline at end of file diff --git a/static/bidder-params/melozen.json b/static/bidder-params/melozen.json new file mode 100644 index 00000000000..6b5cef5b3fd --- /dev/null +++ b/static/bidder-params/melozen.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MeloZen Adapter Params", + "description": "A schema which validates params accepted by the MeloZen adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "minLength": 1, + "description": "The unique identifier for the publisher." + } + }, + "required": ["pubId"] +} diff --git a/static/bidder-params/missena.json b/static/bidder-params/missena.json new file mode 100644 index 00000000000..c9e20e5a828 --- /dev/null +++ b/static/bidder-params/missena.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Missena Adapter Params", + "description": "A schema which validates params accepted by the Missena adapter", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "API Key", + "minLength": 1 + }, + "placement": { + "type": "string", + "description": "Placement Type (Sticky, Header, ...)" + }, + "test": { + "type": "string", + "description": "Test Mode" + } + }, + "required": [ + "apiKey" + ] +} \ No newline at end of file diff --git a/static/bidder-params/oraki.json b/static/bidder-params/oraki.json new file mode 100644 index 00000000000..610c1f0e8c7 --- /dev/null +++ b/static/bidder-params/oraki.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Oraki Adapter Params", + "description": "A schema which validates params accepted by the Oraki adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] +} diff --git a/static/bidder-params/ownadx.json b/static/bidder-params/ownadx.json index f529e74cb01..e0e09a7e9f7 100644 --- a/static/bidder-params/ownadx.json +++ b/static/bidder-params/ownadx.json @@ -18,10 +18,5 @@ "description": "Token ID" } }, - - "oneOf": [ - { "required": ["sspId"] }, - { "required": ["feedId"] }, - { "required": ["token"] } - ] + "required": ["sspId","seatId","tokenId"] } diff --git a/static/bidder-params/pubrise.json b/static/bidder-params/pubrise.json new file mode 100644 index 00000000000..0d972da45e9 --- /dev/null +++ b/static/bidder-params/pubrise.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Pubrise Adapter Params", + "description": "A schema which validates params accepted by the Pubrise adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] +} \ No newline at end of file diff --git a/stored_requests/data/by_id/accounts/test.json b/stored_requests/data/by_id/accounts/test.json index 699f6bd1e57..a53f8997f37 100644 --- a/stored_requests/data/by_id/accounts/test.json +++ b/stored_requests/data/by_id/accounts/test.json @@ -49,4 +49,4 @@ } } } -} +} \ No newline at end of file diff --git a/stored_responses/stored_responses.go b/stored_responses/stored_responses.go index 04dc3decf5a..f7fb79f1c21 100644 --- a/stored_responses/stored_responses.go +++ b/stored_responses/stored_responses.go @@ -22,22 +22,9 @@ type ImpBidderReplaceImpID map[string]map[string]bool type BidderImpReplaceImpID map[string]map[string]bool func InitStoredBidResponses(req *openrtb2.BidRequest, storedBidResponses ImpBidderStoredResp) BidderImpsWithBidResponses { - removeImpsWithStoredResponses(req, storedBidResponses) return buildStoredResp(storedBidResponses) } -// removeImpsWithStoredResponses deletes imps with stored bid resp -func removeImpsWithStoredResponses(req *openrtb2.BidRequest, storedBidResponses ImpBidderStoredResp) { - imps := req.Imp - req.Imp = nil //to indicate this bidder doesn't have real requests - for _, imp := range imps { - if _, ok := storedBidResponses[imp.ID]; !ok { - //add real imp back to request - req.Imp = append(req.Imp, imp) - } - } -} - func buildStoredResp(storedBidResponses ImpBidderStoredResp) BidderImpsWithBidResponses { // bidder -> imp id -> stored bid resp bidderToImpToResponses := BidderImpsWithBidResponses{} diff --git a/stored_responses/stored_responses_test.go b/stored_responses/stored_responses_test.go index 49ce19d3414..99a7bf5ae44 100644 --- a/stored_responses/stored_responses_test.go +++ b/stored_responses/stored_responses_test.go @@ -11,72 +11,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRemoveImpsWithStoredResponses(t *testing.T) { - bidRespId1 := json.RawMessage(`{"id": "resp_id1"}`) - testCases := []struct { - description string - reqIn *openrtb2.BidRequest - storedBidResponses ImpBidderStoredResp - expectedImps []openrtb2.Imp - }{ - { - description: "request with imps and stored bid response for this imp", - reqIn: &openrtb2.BidRequest{Imp: []openrtb2.Imp{ - {ID: "imp-id1"}, - }}, - storedBidResponses: ImpBidderStoredResp{ - "imp-id1": {"appnexus": bidRespId1}, - }, - expectedImps: nil, - }, - { - description: "request with imps and stored bid response for one of these imp", - reqIn: &openrtb2.BidRequest{Imp: []openrtb2.Imp{ - {ID: "imp-id1"}, - {ID: "imp-id2"}, - }}, - storedBidResponses: ImpBidderStoredResp{ - "imp-id1": {"appnexus": bidRespId1}, - }, - expectedImps: []openrtb2.Imp{ - { - ID: "imp-id2", - }, - }, - }, - { - description: "request with imps and stored bid response for both of these imp", - reqIn: &openrtb2.BidRequest{Imp: []openrtb2.Imp{ - {ID: "imp-id1"}, - {ID: "imp-id2"}, - }}, - storedBidResponses: ImpBidderStoredResp{ - "imp-id1": {"appnexus": bidRespId1}, - "imp-id2": {"appnexus": bidRespId1}, - }, - expectedImps: nil, - }, - { - description: "request with imps and no stored bid responses", - reqIn: &openrtb2.BidRequest{Imp: []openrtb2.Imp{ - {ID: "imp-id1"}, - {ID: "imp-id2"}, - }}, - storedBidResponses: nil, - - expectedImps: []openrtb2.Imp{ - {ID: "imp-id1"}, - {ID: "imp-id2"}, - }, - }, - } - for _, testCase := range testCases { - request := testCase.reqIn - removeImpsWithStoredResponses(request, testCase.storedBidResponses) - assert.Equal(t, testCase.expectedImps, request.Imp, "incorrect Impressions for testCase %s", testCase.description) - } -} - func TestBuildStoredBidResponses(t *testing.T) { bidRespId1 := json.RawMessage(`{"id": "resp_id1"}`) bidRespId2 := json.RawMessage(`{"id": "resp_id2"}`) diff --git a/usersync/chooser.go b/usersync/chooser.go index d8bf731f693..462ed635e5a 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -86,8 +86,8 @@ const ( // StatusUnknownBidder specifies a requested bidder is unknown to Prebid Server. StatusUnknownBidder - // StatusTypeNotSupported specifies a requested sync type is not supported by a specific bidder. - StatusTypeNotSupported + // StatusRejectedByFilter specifies a requested sync type is not supported by a specific bidder. + StatusRejectedByFilter // StatusDuplicate specifies the bidder is a duplicate or shared a syncer key with another bidder choice. StatusDuplicate @@ -181,7 +181,7 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{} syncersSeen[syncer.Key()] = struct{}{} if !syncer.SupportsType(syncTypeFilter.ForBidder(strings.ToLower(bidder))) { - return nil, BidderEvaluation{Status: StatusTypeNotSupported, Bidder: bidder, SyncerKey: syncer.Key()} + return nil, BidderEvaluation{Status: StatusRejectedByFilter, Bidder: bidder, SyncerKey: syncer.Key()} } if cookie.HasLiveSync(syncer.Key()) { diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go index f48dbeff9f1..9e57f1da4ce 100644 --- a/usersync/chooser_test.go +++ b/usersync/chooser_test.go @@ -153,7 +153,7 @@ func TestChooserChoose(t *testing.T) { bidderNamesLookup: normalizedBidderNamesLookup, expected: Result{ Status: StatusOK, - BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}}, + BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusRejectedByFilter}}, SyncersChosen: []SyncerChoice{}, }, }, @@ -228,7 +228,7 @@ func TestChooserChoose(t *testing.T) { bidderNamesLookup: normalizedBidderNamesLookup, expected: Result{ Status: StatusOK, - BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}, {Bidder: "a", SyncerKey: "keyA", Status: StatusOK}}, + BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusRejectedByFilter}, {Bidder: "a", SyncerKey: "keyA", Status: StatusOK}}, SyncersChosen: []SyncerChoice{syncerChoiceA}, }, }, @@ -243,7 +243,7 @@ func TestChooserChoose(t *testing.T) { bidderNamesLookup: normalizedBidderNamesLookup, expected: Result{ Status: StatusOK, - BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}}, + BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "c", SyncerKey: "keyC", Status: StatusRejectedByFilter}}, SyncersChosen: []SyncerChoice{syncerChoiceA}, }, }, @@ -531,7 +531,7 @@ func TestChooserEvaluate(t *testing.T) { givenSyncTypeFilter: syncTypeFilter, normalizedBidderNamesLookup: normalizedBidderNamesLookup, expectedSyncer: nil, - expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported}, + expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusRejectedByFilter}, }, { description: "Already Synced",