diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 2a870d9e2f6..012a2d8b501 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -34,6 +34,7 @@ For modules and core platform updates, the initial reviewer should request an ad - Adapters may not use the $$PREBID_GLOBAL$$ variable - All adapters must support the creation of multiple concurrent instances. This means, for example, that adapters cannot rely on mutable global variables. - Adapters may not globally override or default the standard ad server targeting values: hb_adid, hb_bidder, hb_pb, hb_deal, or hb_size, hb_source, hb_format. +- After a new adapter is approved, let the submitter know they may open a PR in the [headerbid-expert repository](https://github.com/prebid/headerbid-expert) to have their adapter recognized by the [Headerbid Expert extension](https://chrome.google.com/webstore/detail/headerbid-expert/cgfkddgbnfplidghapbbnngaogeldmop). The PR should be to the [bidder patterns file](https://github.com/prebid/headerbid-expert/blob/master/bidderPatterns.js), adding an entry with their adapter's name and the url the adapter uses to send and receive bid responses. ## Ticket Coordinator diff --git a/integrationExamples/gpt/gdpr_hello_world.html b/integrationExamples/gpt/gdpr_hello_world.html new file mode 100644 index 00000000000..9f6194edb16 --- /dev/null +++ b/integrationExamples/gpt/gdpr_hello_world.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + \ No newline at end of file diff --git a/modules/adformBidAdapter.js b/modules/adformBidAdapter.js index 10a4696d755..a84def819c1 100644 --- a/modules/adformBidAdapter.js +++ b/modules/adformBidAdapter.js @@ -10,7 +10,7 @@ export const spec = { isBidRequestValid: function (bid) { return !!(bid.params.mid); }, - buildRequests: function (validBidRequests) { + buildRequests: function (validBidRequests, bidderRequest) { var i, l, j, k, bid, _key, _value, reqParams, netRevenue; var request = []; var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ] ]; @@ -38,6 +38,11 @@ export const spec = { request.push('pt=' + netRevenue); request.push('stid=' + validBidRequests[0].auctionId); + if (bidderRequest && bidderRequest.gdprConsent) { + request.push('gdpr=' + bidderRequest.gdprConsent.gdprApplies); + request.push('gdpr_consent=' + bidderRequest.gdprConsent.consentString); + } + for (i = 1, l = globalParams.length; i < l; i++) { _key = globalParams[i][0]; _value = globalParams[i][1]; diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 0fb5aa1a4d3..18d30685c56 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; import { config } from 'src/config'; import { EVENTS } from 'src/constants.json'; +import { BANNER } from 'src/mediaTypes'; const AOL_BIDDERS_CODES = { AOL: 'aol', @@ -30,9 +31,9 @@ const SYNC_TYPES = { } }; -const pubapiTemplate = template`//${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'}${'bidfloor'}${'keyValues'};misc=${'misc'}`; +const pubapiTemplate = template`//${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'};misc=${'misc'}${'bidfloor'}${'keyValues'}${'consentData'}`; const nexageBaseApiTemplate = template`//${'host'}/bidRequest?`; -const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'ext'}`; +const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'dynamicParams'}`; const MP_SERVER_MAP = { us: 'adserver-us.adtech.advertising.com', eu: 'adserver-eu.adtech.advertising.com', @@ -46,10 +47,10 @@ $$PREBID_GLOBAL$$.aolGlobals = { pixelsDropped: false }; -let showCpmAdjustmentWarning = (function () { +let showCpmAdjustmentWarning = (function() { let showCpmWarning = true; - return function () { + return function() { let bidderSettings = $$PREBID_GLOBAL$$.bidderSettings; if (showCpmWarning && bidderSettings && bidderSettings.aol && typeof bidderSettings.aol.bidCpmAdjustment === 'function') { @@ -62,28 +63,18 @@ let showCpmAdjustmentWarning = (function () { }; })(); -function isInteger(value) { - return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value; -} - function template(strings, ...keys) { return function(...values) { let dict = values[values.length - 1] || {}; let result = [strings[0]]; keys.forEach(function(key, i) { - let value = isInteger(key) ? values[key] : dict[key]; + let value = utils.isInteger(key) ? values[key] : dict[key]; result.push(value, strings[i + 1]); }); return result.join(''); }; } -function isSecureProtocol() { - return document.location.protocol === 'https:'; -} - function parsePixelItems(pixels) { let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi; let tagNameRegExp = /\w*(?=\s)/; @@ -110,39 +101,6 @@ function parsePixelItems(pixels) { return pixelsItems; } -function _buildMarketplaceUrl(bid) { - const params = bid.params; - const serverParam = params.server; - let regionParam = params.region || 'us'; - let server; - - if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) { - utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`); - regionParam = 'us'; // Default region. - } - - if (serverParam) { - server = serverParam; - } else { - server = MP_SERVER_MAP[regionParam]; - } - - // Set region param, used by AOL analytics. - params.region = regionParam; - - return pubapiTemplate({ - host: server, - network: params.network, - placement: parseInt(params.placement), - pageid: params.pageId || 0, - sizeid: params.sizeId || 0, - alias: params.alias || utils.getUniqueIdentifierStr(), - bidfloor: formatMarketplaceBidFloor(params.bidFloor), - keyValues: formatMarketplaceKeyValues(params.keyValues), - misc: new Date().getTime() // cache busting - }); -} - function formatMarketplaceBidFloor(bidFloor) { return (typeof bidFloor !== 'undefined') ? `;bidfloor=${bidFloor.toString()}` : ''; } @@ -157,39 +115,16 @@ function formatMarketplaceKeyValues(keyValues) { return formattedKeyValues; } -function _buildOneMobileBaseUrl(bid) { - return nexageBaseApiTemplate({ - host: bid.params.host || NEXAGE_SERVER - }); -} - -function _buildOneMobileGetUrl(bid) { - let {dcn, pos} = bid.params; - let nexageApi = _buildOneMobileBaseUrl(bid); - if (dcn && pos) { - let ext = ''; - if (isSecureProtocol()) { - bid.params.ext = bid.params.ext || {}; - bid.params.ext.secure = 1; - } - utils._each(bid.params.ext, (value, key) => { - ext += `&${key}=${encodeURIComponent(value)}`; - }); - nexageApi += nexageGetApiTemplate({dcn, pos, ext}); - } - return nexageApi; -} - function _isMarketplaceBidder(bidder) { return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEDISPLAY; } -function _isNexageBidder(bidder) { - return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEMOBILE; +function _isOneMobileBidder(bidderCode) { + return bidderCode === AOL_BIDDERS_CODES.AOL || bidderCode === AOL_BIDDERS_CODES.ONEMOBILE; } function _isNexageRequestPost(bid) { - if (_isNexageBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) { + if (_isOneMobileBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) { let imp = bid.params.imp[0]; return imp.id && imp.tagid && ((imp.banner && imp.banner.w && imp.banner.h) || @@ -198,7 +133,7 @@ function _isNexageRequestPost(bid) { } function _isNexageRequestGet(bid) { - return _isNexageBidder(bid.bidder) && bid.params.dcn && bid.params.pos; + return _isOneMobileBidder(bid.bidder) && bid.params.dcn && bid.params.pos; } function isMarketplaceBid(bid) { @@ -219,65 +154,25 @@ function resolveEndpointCode(bid) { } } -function formatBidRequest(endpointCode, bid) { - let bidRequest; - - switch (endpointCode) { - case AOL_ENDPOINTS.DISPLAY.GET: - bidRequest = { - url: _buildMarketplaceUrl(bid), - method: 'GET', - ttl: ONE_DISPLAY_TTL - }; - break; - - case AOL_ENDPOINTS.MOBILE.GET: - bidRequest = { - url: _buildOneMobileGetUrl(bid), - method: 'GET', - ttl: ONE_MOBILE_TTL - }; - break; - - case AOL_ENDPOINTS.MOBILE.POST: - bidRequest = { - url: _buildOneMobileBaseUrl(bid), - method: 'POST', - ttl: ONE_MOBILE_TTL, - data: bid.params, - options: { - contentType: 'application/json', - customHeaders: { - 'x-openrtb-version': '2.2' - } - } - }; - break; - } - - bidRequest.bidderCode = bid.bidder; - bidRequest.bidId = bid.bidId; - bidRequest.userSyncOn = bid.params.userSyncOn; - - return bidRequest; -} - export const spec = { code: AOL_BIDDERS_CODES.AOL, aliases: [AOL_BIDDERS_CODES.ONEMOBILE, AOL_BIDDERS_CODES.ONEDISPLAY], - isBidRequestValid: function(bid) { + supportedMediaTypes: [BANNER], + isBidRequestValid(bid) { return isMarketplaceBid(bid) || isMobileBid(bid); }, - buildRequests: function (bids) { + buildRequests(bids, bidderRequest) { + let consentData = bidderRequest ? bidderRequest.gdprConsent : null; + return bids.map(bid => { const endpointCode = resolveEndpointCode(bid); if (endpointCode) { - return formatBidRequest(endpointCode, bid); + return this.formatBidRequest(endpointCode, bid, consentData); } }); }, - interpretResponse: function ({body}, bidRequest) { + interpretResponse({body}, bidRequest) { showCpmAdjustmentWarning(); if (!body) { @@ -290,17 +185,157 @@ export const spec = { } } }, - _formatPixels: function (pixels) { - let formattedPixels = pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, ''); + getUserSyncs(options, bidResponses) { + let bidResponse = bidResponses[0]; - return ''; + if (config.getConfig('aol.userSyncOn') === EVENTS.BID_RESPONSE) { + if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse && bidResponse.ext && bidResponse.ext.pixels) { + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; + + return parsePixelItems(bidResponse.ext.pixels); + } + } + + return []; + }, + + formatBidRequest(endpointCode, bid, consentData) { + let bidRequest; + + switch (endpointCode) { + case AOL_ENDPOINTS.DISPLAY.GET: + bidRequest = { + url: this.buildMarketplaceUrl(bid, consentData), + method: 'GET', + ttl: ONE_DISPLAY_TTL + }; + break; + + case AOL_ENDPOINTS.MOBILE.GET: + bidRequest = { + url: this.buildOneMobileGetUrl(bid, consentData), + method: 'GET', + ttl: ONE_MOBILE_TTL + }; + break; + + case AOL_ENDPOINTS.MOBILE.POST: + bidRequest = { + url: this.buildOneMobileBaseUrl(bid), + method: 'POST', + ttl: ONE_MOBILE_TTL, + data: this.buildOpenRtbRequestData(bid, consentData), + options: { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.2' + } + } + }; + break; + } + + bidRequest.bidderCode = bid.bidder; + bidRequest.bidId = bid.bidId; + bidRequest.userSyncOn = bid.params.userSyncOn; + + return bidRequest; }, - _parseBidResponse: function (response, bidRequest) { + buildMarketplaceUrl(bid, consentData) { + const params = bid.params; + const serverParam = params.server; + let regionParam = params.region || 'us'; + let server; + + if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) { + utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`); + regionParam = 'us'; // Default region. + } + + if (serverParam) { + server = serverParam; + } else { + server = MP_SERVER_MAP[regionParam]; + } + + // Set region param, used by AOL analytics. + params.region = regionParam; + + return pubapiTemplate({ + host: server, + network: params.network, + placement: parseInt(params.placement), + pageid: params.pageId || 0, + sizeid: params.sizeId || 0, + alias: params.alias || utils.getUniqueIdentifierStr(), + misc: new Date().getTime(), // cache busting, + bidfloor: formatMarketplaceBidFloor(params.bidFloor), + keyValues: formatMarketplaceKeyValues(params.keyValues), + consentData: this.formatMarketplaceConsentData(consentData) + }); + }, + buildOneMobileGetUrl(bid, consentData) { + let {dcn, pos, ext} = bid.params; + let nexageApi = this.buildOneMobileBaseUrl(bid); + if (dcn && pos) { + let dynamicParams = this.formatOneMobileDynamicParams(ext, consentData); + nexageApi += nexageGetApiTemplate({dcn, pos, dynamicParams}); + } + return nexageApi; + }, + buildOneMobileBaseUrl(bid) { + return nexageBaseApiTemplate({ + host: bid.params.host || NEXAGE_SERVER + }); + }, + formatOneMobileDynamicParams(params = {}, consentData) { + if (this.isSecureProtocol()) { + params.secure = 1; + } + + if (this.isConsentRequired(consentData)) { + params.euconsent = consentData.consentString; + params.gdpr = 1; + } + + let paramsFormatted = ''; + utils._each(params, (value, key) => { + paramsFormatted += `&${key}=${encodeURIComponent(value)}`; + }); + + return paramsFormatted; + }, + buildOpenRtbRequestData(bid, consentData) { + let openRtbObject = { + id: bid.params.id, + imp: bid.params.imp + }; + + if (this.isConsentRequired(consentData)) { + openRtbObject.user = { + ext: { + consent: consentData.consentString + } + }; + openRtbObject.regs = { + ext: { + gdpr: 1 + } + }; + } + + return openRtbObject; + }, + isConsentRequired(consentData) { + return !!(consentData && consentData.consentString && consentData.gdprApplies); + }, + formatMarketplaceConsentData(consentData) { + let consentRequired = this.isConsentRequired(consentData); + + return consentRequired ? `;euconsent=${consentData.consentString};gdpr=1` : ''; + }, + + _parseBidResponse(response, bidRequest) { let bidData; try { @@ -322,17 +357,10 @@ export const spec = { } } - let ad = bidData.adm; - if (response.ext && response.ext.pixels) { - if (config.getConfig('aol.userSyncOn') !== EVENTS.BID_RESPONSE) { - ad += this._formatPixels(response.ext.pixels); - } - } - - return { + let bidResponse = { bidderCode: bidRequest.bidderCode, requestId: bidRequest.bidId, - ad: ad, + ad: bidData.adm, cpm: cpm, width: bidData.w, height: bidData.h, @@ -343,19 +371,28 @@ export const spec = { netRevenue: true, ttl: bidRequest.ttl }; - }, - getUserSyncs: function(options, bidResponses) { - let bidResponse = bidResponses[0]; - if (config.getConfig('aol.userSyncOn') === EVENTS.BID_RESPONSE) { - if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse.ext && bidResponse.ext.pixels) { - $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; - - return parsePixelItems(bidResponse.ext.pixels); + if (response.ext && response.ext.pixels) { + if (config.getConfig('aol.userSyncOn') !== EVENTS.BID_RESPONSE) { + bidResponse.ad += this.formatPixels(response.ext.pixels); } } - return []; + return bidResponse; + }, + formatPixels(pixels) { + let formattedPixels = pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, ''); + + return ''; + }, + isOneMobileBidder: _isOneMobileBidder, + isSecureProtocol() { + return document.location.protocol === 'https:'; } }; diff --git a/modules/aolBidAdapter.md b/modules/aolBidAdapter.md index a92e933bd36..8a9d1e3291d 100644 --- a/modules/aolBidAdapter.md +++ b/modules/aolBidAdapter.md @@ -22,7 +22,6 @@ Module that connects to AOL's demand sources params: { placement: '3611253', network: '9599.1', - bidFloor: '0.80', keyValues: { test: 'key' } diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 75e48d1ee0b..82743974994 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -73,6 +73,15 @@ export const spec = { if (member > 0) { payload.member_id = member; } + + if (bidderRequest && bidderRequest.gdprConsent) { + // note - objects for impbus use underscore instead of camelCase + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + } + const payloadString = JSON.stringify(payload); return { method: 'POST', diff --git a/modules/consentManagement.js b/modules/consentManagement.js new file mode 100644 index 00000000000..c7b6ac4df92 --- /dev/null +++ b/modules/consentManagement.js @@ -0,0 +1,278 @@ +/** + * This module adds GDPR consentManagement support to prebid.js. It interacts with + * supported CMPs (Consent Management Platforms) to grab the user's consent information + * and make it available for any GDPR supported adapters to read/pass this information to + * their system. + */ +import * as utils from 'src/utils'; +import { config } from 'src/config'; +import { gdprDataHandler } from 'src/adaptermanager'; +import includes from 'core-js/library/fn/array/includes'; + +const DEFAULT_CMP = 'iab'; +const DEFAULT_CONSENT_TIMEOUT = 10000; +const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; + +export let userCMP; +export let consentTimeout; +export let allowAuction; + +let consentData; + +let context; +let args; +let nextFn; + +let timer; +let haveExited; + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent +}; + +/** + * This function handles interacting with an IAB compliant CMP to obtain the consentObject value of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP + * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) + */ +function lookupIabConsent(cmpSuccess, cmpError) { + let cmpCallbacks; + + // check if the CMP is located on the same window level as the prebid code. + // if it's found, directly call the CMP via it's API and call the cmpSuccess callback. + // if it's not found, assume the prebid code may be inside an iframe and the CMP code is located in a higher parent window. + // in this case, use the IAB's iframe locator sample code (which is slightly cutomized) to try to find the CMP and use postMessage() to communicate with the CMP. + if (utils.isFn(window.__cmp)) { + window.__cmp('getVendorConsents', null, cmpSuccess); + } else { + callCmpWhileInIframe(); + } + + function callCmpWhileInIframe() { + /** + * START OF STOCK CODE FROM IAB 1.1 CMP SPEC + */ + + // find the CMP frame + let f = window; + let cmpFrame; + while (!cmpFrame) { + try { + if (f.frames['__cmpLocator']) cmpFrame = f; + } catch (e) {} + if (f === window.top) break; + f = f.parent; + } + + cmpCallbacks = {}; + + /* Setup up a __cmp function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ + window.__cmp = function(cmd, arg, callback) { + if (!cmpFrame) { + removePostMessageListener(); + + let errmsg = 'CMP not found'; + // small customization to properly return error + return cmpError(errmsg); + } + let callId = Math.random() + ''; + let msg = {__cmpCall: { + command: cmd, + parameter: arg, + callId: callId + }}; + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + // small customization to remove this eventListener later in module + window.addEventListener('message', readPostMessageResponse, false); + + /** + * END OF STOCK CODE FROM IAB 1.1 CMP SPEC + */ + + // call CMP + window.__cmp('getVendorConsents', null, cmpIframeCallback); + } + + function readPostMessageResponse(event) { + // small customization to prevent reading strings from other sources that aren't JSON.stringified + let json = (typeof event.data === 'string' && includes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data; + if (json.__cmpReturn) { + let i = json.__cmpReturn; + cmpCallbacks[i.callId](i.returnValue, i.success); + delete cmpCallbacks[i.callId]; + } + } + + function removePostMessageListener() { + window.removeEventListener('message', readPostMessageResponse, false); + } + + function cmpIframeCallback(consentObject) { + removePostMessageListener(); + cmpSuccess(consentObject); + } +} + +/** + * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported CMP. Once obtained, the module will store this + * data as part of a gdprConsent object which gets transferred to adaptermanager's gdprDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @param {object} config required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export function requestBidsHook(config, fn) { + context = this; + args = arguments; + nextFn = fn; + haveExited = false; + + // in case we already have consent (eg during bid refresh) + if (consentData) { + return exitModule(); + } + + if (!includes(Object.keys(cmpCallMap), userCMP)) { + utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return nextFn.apply(context, args); + } + + cmpCallMap[userCMP].call(this, processCmpData, cmpFailed); + + // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) + if (!haveExited) { + if (consentTimeout === 0) { + processCmpData(undefined); + } else { + timer = setTimeout(cmpTimedOut, consentTimeout); + } + } +} + +/** + * This function checks the consent data provided by CMP to ensure it's in an expected state. + * If it's bad, we exit the module depending on config settings. + * If it's good, then we store the value and exits the module. + * @param {object} consentObject required; object returned by CMP that contains user's consent choices + */ +function processCmpData(consentObject) { + if (!utils.isPlainObject(consentObject) || !utils.isStr(consentObject.metadata) || consentObject.metadata === '') { + cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`); + } else { + clearTimeout(timer); + storeConsentData(consentObject); + + exitModule(); + } +} + +/** + * General timeout callback when interacting with CMP takes too long. + */ +function cmpTimedOut() { + cmpFailed('CMP workflow exceeded timeout threshold.'); +} + +/** + * This function contains the controlled steps to perform when there's a problem with CMP. + * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. +*/ +function cmpFailed(errMsg) { + clearTimeout(timer); + + // still set the consentData to undefined when there is a problem as per config options + if (allowAuction) { + storeConsentData(undefined); + } + exitModule(errMsg); +} + +/** + * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction + * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + */ +function storeConsentData(cmpConsentObject) { + consentData = { + consentString: (cmpConsentObject) ? cmpConsentObject.metadata : undefined, + vendorData: cmpConsentObject, + gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : undefined + }; + gdprDataHandler.setConsentData(consentData); +} + +/** + * This function handles the exit logic for the module. + * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others. + * + * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. + * One scenario could be auction was canceled due to timeout with CMP being reached. + * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit). + * In this case, the good exit will be suppressed since we already decided to cancel the auction. + * + * Three exit paths are: + * 1. good exit where auction runs (CMP data is processed normally). + * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along). + * 3. bad exit with auction canceled (error message is logged). + * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. + */ +function exitModule(errMsg) { + if (haveExited === false) { + haveExited = true; + + if (errMsg) { + if (allowAuction) { + utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.'); + nextFn.apply(context, args); + } else { + utils.logError(errMsg + ' Canceling auction as per consentManagement config.'); + } + } else { + nextFn.apply(context, args); + } + } +} + +/** + * Simply resets the module's consentData variable back to undefined, mainly for testing purposes + */ +export function resetConsentData() { + consentData = undefined; +} + +/** + * A configuration function that initializes some module variables, as well as add a hook into the requestBids function + * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + */ +export function setConfig(config) { + if (utils.isStr(config.cmpApi)) { + userCMP = config.cmpApi; + } else { + userCMP = DEFAULT_CMP; + utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); + } + + if (utils.isNumber(config.timeout)) { + consentTimeout = config.timeout; + } else { + consentTimeout = DEFAULT_CONSENT_TIMEOUT; + utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); + } + + if (typeof config.allowAuctionWithoutConsent === 'boolean') { + allowAuction = config.allowAuctionWithoutConsent; + } else { + allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; + utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + } + + $$PREBID_GLOBAL$$.requestBids.addHook(requestBidsHook, 50); +} +config.getConfig('consentManagement', config => setConfig(config.consentManagement)); diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 0bee2734d01..611e8eebd6e 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,4 +1,5 @@ import { registerBidder } from 'src/adapters/bidderFactory'; +import { isInteger } from 'src/utils'; const BIDDER_CODE = 'getintent'; const IS_NET_REVENUE = true; @@ -142,7 +143,7 @@ function parseSize(s) { * */ function produceSize (sizes) { function sizeToStr(s) { - if (Array.isArray(s) && s.length === 2 && Number.isInteger(s[0]) && Number.isInteger(s[1])) { + if (Array.isArray(s) && s.length === 2 && isInteger(s[0]) && isInteger(s[1])) { return s.join('x'); } else { throw "Malformed parameter 'sizes'"; diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index bc00242b3e3..1cc312da273 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -70,21 +70,10 @@ function createBannerBidResponses(oxResponseObj, {bids, startTime}) { } for (let i = 0; i < adUnits.length; i++) { let adUnit = adUnits[i]; + let adUnitIdx = parseInt(adUnit.idx, 10); let bidResponse = {}; - if (adUnits.length === bids.length) { - // request and response length match, directly assign the request id based on positioning - bidResponse.requestId = bids[i].bidId; - } else { - for (let j = i; j < bids.length; j++) { - let bid = bids[j]; - if (String(bid.params.unit) === String(adUnit.adunitid) && adUnitHasValidSizeFromBid(adUnit, bid) && !bid.matched) { - // ad unit and size match, this is the correct bid response to bid - bidResponse.requestId = bid.bidId; - bid.matched = true; - break; - } - } - } + + bidResponse.requestId = bids[adUnitIdx].bidId; if (adUnit.pub_rev) { bidResponse.cpm = Number(adUnit.pub_rev) / 1000; @@ -134,27 +123,6 @@ function buildQueryStringFromParams(params) { .join('&'); } -function adUnitHasValidSizeFromBid(adUnit, bid) { - let sizes = utils.parseSizesInput(bid.sizes); - if (!sizes) { - return false; - } - let found = false; - let creative = adUnit.creative && adUnit.creative[0]; - let creative_size = String(creative.width) + 'x' + String(creative.height); - - if (utils.isArray(sizes)) { - for (let i = 0; i < sizes.length; i++) { - let size = sizes[i]; - if (String(size) === String(creative_size)) { - found = true; - break; - } - } - } - return found; -} - function getViewportDimensions(isIfr) { let width; let height; diff --git a/modules/pre1api.js b/modules/pre1api.js index 707d10fbfd8..a8aa1f31e70 100644 --- a/modules/pre1api.js +++ b/modules/pre1api.js @@ -124,7 +124,7 @@ pbjs.requestBids.addHook((config, next = config) => { } else { logWarn(`${MODULE_NAME} module: concurrency has been disabled and "$$PREBID_GLOBAL$$.requestBids" call was queued`); } -}, 100); +}, 5); Object.keys(auctionPropMap).forEach(prop => { if (prop === 'allBidsAvailable') { diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 22529def0a9..f499f5a0ae4 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -304,7 +304,7 @@ function transformHeightWidth(adUnit) { */ const LEGACY_PROTOCOL = { - buildRequest(s2sBidRequest, adUnits) { + buildRequest(s2sBidRequest, bidRequests, adUnits) { // pbs expects an ad_unit.video attribute if the imp is video adUnits.forEach(adUnit => { adUnit.sizes = transformHeightWidth(adUnit); @@ -437,7 +437,7 @@ const OPEN_RTB_PROTOCOL = { bidMap: {}, - buildRequest(s2sBidRequest, adUnits) { + buildRequest(s2sBidRequest, bidRequests, adUnits) { let imps = []; let aliases = {}; @@ -530,6 +530,35 @@ const OPEN_RTB_PROTOCOL = { request.ext = { prebid: { aliases } }; } + if (bidRequests && bidRequests[0].gdprConsent) { + // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module + let gdprApplies; + if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0; + } + + if (request.regs) { + if (request.regs.ext) { + request.regs.ext.gdpr = gdprApplies; + } else { + request.regs.ext = { gdpr: gdprApplies }; + } + } else { + request.regs = { ext: { gdpr: gdprApplies } }; + } + + let consentString = bidRequests[0].gdprConsent.consentString; + if (request.user) { + if (request.user.ext) { + request.user.ext.consent = consentString; + } else { + request.user.ext = { consent: consentString }; + } + } else { + request.user = { ext: { consent: consentString } }; + } + } + return request; }, @@ -637,7 +666,7 @@ export function PrebidServer() { .reduce(utils.flatten) .filter(utils.uniques); - const request = protocolAdapter().buildRequest(s2sBidRequest, adUnitsWithSizes); + const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes); const requestJson = JSON.stringify(request); ajax( diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index dfcde047580..1f056bf0eff 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -19,6 +19,11 @@ const CUSTOM_PARAMS = { 'verId': '' // OpenWrap Legacy: version ID }; const NET_REVENUE = false; +const dealChannelValues = { + 1: 'PMP', + 5: 'PREF', + 6: 'PMPG' +}; let publisherId = 0; @@ -195,7 +200,7 @@ export const spec = { * @param {validBidRequests[]} - an array of bids * @return ServerRequest Info describing the request to the server. */ - buildRequests: validBidRequests => { + buildRequests: (validBidRequests, bidderRequest) => { var conf = _initConf(); var payload = _createOrtbTemplate(conf); validBidRequests.forEach(bid => { @@ -217,14 +222,28 @@ export const spec = { payload.site.publisher.id = conf.pubId.trim(); publisherId = conf.pubId.trim(); payload.ext.wrapper = {}; - payload.ext.wrapper.profile = conf.profId || UNDEFINED; - payload.ext.wrapper.version = conf.verId || UNDEFINED; + payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED; + payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED; payload.ext.wrapper.wiid = conf.wiid || UNDEFINED; payload.ext.wrapper.wv = constants.REPO_AND_VERSION; payload.ext.wrapper.transactionId = conf.transactionId; payload.ext.wrapper.wp = 'pbjs'; payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; + + // Attaching GDPR Consent Params + if (bidderRequest && bidderRequest.gdprConsent) { + payload.user.ext = { + consent: bidderRequest.gdprConsent.consentString + }; + + payload.regs = { + ext: { + gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) + } + }; + } + payload.user.geo.lat = _parseSlotParam('lat', conf.lat); payload.user.geo.lon = _parseSlotParam('lon', conf.lon); payload.user.yob = _parseSlotParam('yob', conf.yob); @@ -264,6 +283,11 @@ export const spec = { referrer: utils.getTopWindowUrl(), ad: bid.adm }; + + if (bid.ext && bid.ext.deal_channel) { + newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; + } + bidResponses.push(newBid); }); } @@ -276,11 +300,19 @@ export const spec = { /** * Register User Sync. */ - getUserSyncs: syncOptions => { + getUserSyncs: (syncOptions, responses, gdprConsent) => { + let syncurl = USYNCURL + publisherId; + + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: USYNCURL + publisherId + url: syncurl }]; } else { utils.logWarn('PubMatic: Please enable iframe based user sync.'); diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index fc637cc9fff..94733ad7805 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -34,7 +34,7 @@ export const spec = { !!(bid && bid.params && bid.params.cp && bid.params.ct) ), - buildRequests: bidRequests => { + buildRequests: (bidRequests, bidderRequest) => { const request = { id: bidRequests[0].bidderRequestId, imp: bidRequests.map(slot => impression(slot)), @@ -42,6 +42,7 @@ export const spec = { app: app(bidRequests), device: device(), }; + applyGdpr(bidderRequest, request); return { method: 'POST', url: '//bid.contextweb.com/header/ortb', @@ -304,6 +305,16 @@ function adSize(slot) { return [1, 1]; } +/** + * Applies GDPR parameters to request. + */ +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; + ortbRequest.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + } +} + /** * Parses the native response from the Bid given. */ diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index ea88886b753..bf2930e5684 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -132,8 +132,6 @@ export const spec = { page_url = utils.getTopWindowUrl(); } - page_url = bidRequest.params.secure ? page_url.replace(/^http:/i, 'https:') : page_url; - // GDPR reference, for use by 'banner' and 'video' const gdprConsent = bidderRequest.gdprConsent; @@ -150,6 +148,7 @@ export const spec = { timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), stash_creatives: true, ae_pass_through_parameters: params.video.aeParams, + rp_secure: bidRequest.params.secure !== false, slots: [] }; diff --git a/modules/sekindoUMBidAdapter.js b/modules/sekindoUMBidAdapter.js index e87f3194ff0..cf8ba9e23f0 100644 --- a/modules/sekindoUMBidAdapter.js +++ b/modules/sekindoUMBidAdapter.js @@ -25,11 +25,17 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { var pubUrl = null; - if (parent !== window) { - pubUrl = document.referrer; - } else { - pubUrl = window.location.href; - } + try { + if (window.top == window) { + pubUrl = window.location.href; + } else { + try { + pubUrl = window.top.location.href; + } catch (e2) { + pubUrl = document.referrer; + } + } + } catch (e1) {} return validBidRequests.map(bidRequest => { var subId = utils.getBidIdParameter('subId', bidRequest.params); diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 564dca85690..6d67288cf09 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -21,7 +21,7 @@ export const spec = { * @param {BidRequest[]} bidRequests Array of Sovrn bidders * @return object of parameters for Prebid AJAX request */ - buildRequests: function(bidReqs) { + buildRequests: function(bidReqs, bidderRequest) { const loc = utils.getTopWindowLocation(); let sovrnImps = []; let iv; @@ -44,6 +44,17 @@ export const spec = { }; if (iv) sovrnBidReq.iv = iv; + if (bidderRequest && bidderRequest.gdprConsent) { + sovrnBidReq.regs = { + ext: { + gdpr: +bidderRequest.gdprConsent.gdprApplies + }}; + sovrnBidReq.user = { + ext: { + consent: bidderRequest.gdprConsent.consentString + }}; + } + return { method: 'POST', url: `//ap.lijit.com/rtb/bid?src=${REPO_AND_VERSION}`, @@ -70,7 +81,7 @@ export const spec = { cpm: parseFloat(sovrnBid.price), width: parseInt(sovrnBid.w), height: parseInt(sovrnBid.h), - creativeId: sovrnBid.id, + creativeId: sovrnBid.crid || sovrnBid.id, dealId: sovrnBid.dealid || null, currency: 'USD', netRevenue: true, diff --git a/package.json b/package.json index bb8afc62998..44c54b468f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "1.9.0-pre", + "version": "1.10.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/adaptermanager.js b/src/adaptermanager.js index cef1635f100..ff08a0caa07 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -1,7 +1,7 @@ /** @module adaptermanger */ import { flatten, getBidderCodes, getDefinedParams, shuffle, timestamp } from './utils'; -import { resolveStatus } from './sizeMapping'; +import { resolveStatus, resolveBidOverrideSizes } from './sizeMapping'; import { processNativeAdUnitParams, nativeAdapters } from './native'; import { newBidder } from './adapters/bidderFactory'; import { ajaxBuilder } from 'src/ajax'; @@ -75,7 +75,10 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels}) { 'renderer' ])); - let {active, sizes} = resolveStatus(getLabels(bid, labels), filteredAdUnitSizes); + // filter any per-bid override sizes + let filteredBidSizes = resolveBidOverrideSizes(bid, filteredAdUnitSizes); + + let {active, sizes} = resolveStatus(getLabels(bid, labels), filteredBidSizes); if (active) { bids.push(Object.assign({}, bid, { @@ -133,6 +136,16 @@ function getAdUnitCopyForClientAdapters(adUnits) { return adUnitsClientCopy; } +exports.gdprDataHandler = { + consentData: null, + setConsentData: function(consentInfo) { + this.consentData = consentInfo; + }, + getConsentData: function() { + return this.consentData; + } +}; + exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) { let bidRequests = []; @@ -197,6 +210,12 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, bidRequests.push(bidderRequest); } }); + + if (exports.gdprDataHandler.getConsentData()) { + bidRequests.forEach(bidRequest => { + bidRequest['gdprConsent'] = exports.gdprDataHandler.getConsentData(); + }); + } return bidRequests; }; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 7540a3a3398..958173a0965 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -191,7 +191,7 @@ export function newBidder(spec) { // As soon as that is refactored, we can move this emit event where it should be, within the done function. events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); - registerSyncs(responses); + registerSyncs(responses, bidderRequest.gdprConsent); } const validBidRequests = bidderRequest.bids.filter(filterAndWarn); @@ -327,12 +327,12 @@ export function newBidder(spec) { } }); - function registerSyncs(responses) { + function registerSyncs(responses, gdprConsent) { if (spec.getUserSyncs) { let syncs = spec.getUserSyncs({ iframeEnabled: config.getConfig('userSync.iframeEnabled'), pixelEnabled: config.getConfig('userSync.pixelEnabled'), - }, responses); + }, responses, gdprConsent); if (syncs) { if (!Array.isArray(syncs)) { syncs = [syncs]; diff --git a/src/adloader.js b/src/adloader.js index 6f2bd112712..e0f2ba46cff 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -1,8 +1,14 @@ -var utils = require('./utils'); -let _requestCache = {}; +import includes from 'core-js/library/fn/array/includes'; +import * as utils from './utils'; + +const _requestCache = {}; +const _vendorWhitelist = [ + 'criteo', +] /** * Loads external javascript. Can only be used if external JS is approved by Prebid. See https://github.com/prebid/prebid-js-external-js-template#policy + * Each unique URL will be loaded at most 1 time. * @param {string} url the url to load * @param {string} moduleCode bidderCode or module code of the module requesting this resource */ @@ -11,18 +17,23 @@ exports.loadExternalScript = function(url, moduleCode) { utils.logError('cannot load external script without url and moduleCode'); return; } + if (!includes(_vendorWhitelist, moduleCode)) { + utils.logError(`${moduleCode} not whitelisted for loading external JavaScript`); + return; + } + // only load each asset once + if (_requestCache[url]) { + return; + } + utils.logWarn(`module ${moduleCode} is loading external JavaScript`); const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; - script.src = url; - // add the new script tag to the page - const target = document.head || document.body; - if (target) { - target.appendChild(script); - } + utils.insertElement(script); + _requestCache[url] = true; }; /** diff --git a/src/sizeMapping.js b/src/sizeMapping.js index 5533c6b4efe..7bb53c55615 100644 --- a/src/sizeMapping.js +++ b/src/sizeMapping.js @@ -38,7 +38,10 @@ export function resolveStatus({labels = [], labelAll = false, activeLabels = []} let filteredSizes; if (maps.shouldFilter) { - filteredSizes = sizes.filter(size => maps.sizesSupported[size]); + filteredSizes = sizes.filter(size => { + // size can be structured in object format IE { w: number, h: number } + return Array.isArray(size) ? maps.sizesSupported[size] : maps.sizesSupported[`${size.w},${size.h}`] + }); } else { filteredSizes = sizes; } @@ -87,3 +90,27 @@ function evaluateSizeConfig(configs) { shouldFilter: false }); } + +/** + * If a bid has a sizes array defined, filter values that exist in bid.sizes from sizes + * @param {{sizes:number}} bid - bid to resolve sizes for + * @param {Array.>|Array.<{w:number, h:number}>} sizes - adUnit sizes + * @returns {Array.>} - sizes filtered using bid.sizes + */ +export function resolveBidOverrideSizes(bid, sizes) { + let filteredSizes; + if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + filteredSizes = sizes.filter(size => { + // size can be structured in object format IE { w: number, h: number } + return Array.isArray(size) ? bid.sizes.some(bidSize => (bidSize[0] === size[0] && bidSize[1] === size[1])) : bid.sizes.some(bidSize => (bidSize[0] === size.w && bidSize[1] === size.h)); + }); + // bid sizes contained invalid sizes if sizes are empty after filtering + if (filteredSizes.length === 0) { + logWarn('Invalid bid override sizes', bid); + filteredSizes = sizes; + } + } else { + filteredSizes = sizes; + } + return filteredSizes; +} diff --git a/src/utils.js b/src/utils.js index 5b8508e52e4..169c578a356 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,6 +11,7 @@ var t_Arr = 'Array'; var t_Str = 'String'; var t_Fn = 'Function'; var t_Numb = 'Number'; +var t_Object = 'Object'; var toString = Object.prototype.toString; let infoLogger = null; try { @@ -382,6 +383,10 @@ exports.isNumber = function(object) { return this.isA(object, t_Numb); }; +exports.isPlainObject = function(object) { + return this.isA(object, t_Object); +} + /** * Return if the object is "empty"; * this includes falsey, no keys, or no items at indices diff --git a/test/spec/adloader_spec.js b/test/spec/adloader_spec.js index 951631d7eac..55224cb0aab 100644 --- a/test/spec/adloader_spec.js +++ b/test/spec/adloader_spec.js @@ -1,4 +1,31 @@ +import * as utils from 'src/utils'; +import * as adLoader from 'src/adloader'; + describe('adLoader', function () { - var assert = require('chai').assert, - adLoader = require('../../src/adloader'); + let utilsinsertElementStub; + let utilsLogErrorStub; + + beforeEach(() => { + utilsinsertElementStub = sinon.stub(utils, 'insertElement'); + utilsLogErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + utilsinsertElementStub.restore(); + utilsLogErrorStub.restore(); + }); + + describe('loadExternalScript', () => { + it('requires moduleCode to be included on the request', () => { + adLoader.loadExternalScript('someURL'); + expect(utilsLogErrorStub.called).to.be.true; + expect(utilsinsertElementStub.called).to.be.false; + }); + + it('only allows whitelisted vendors to load scripts', () => { + adLoader.loadExternalScript('someURL', 'criteo'); + expect(utilsLogErrorStub.called).to.be.false; + expect(utilsinsertElementStub.called).to.be.true; + }); + }); }); diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index d631234d6d5..21ff84bdad5 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -99,6 +99,15 @@ describe('Adform adapter', () => { assert.deepEqual(resultBids, bids[0]); }); + it('should send GDPR Consent data to adform', () => { + var resultBids = JSON.parse(JSON.stringify(bids[0])); + let request = spec.buildRequests([bids[0]], {gdprConsent: {gdprApplies: 1, consentString: 'concentDataString'}}); + let parsedUrl = parseUrl(request.url).query; + + assert.equal(parsedUrl.gdpr, 1); + assert.equal(parsedUrl.gdpr_consent, 'concentDataString'); + }); + it('should set gross to the request, if there is any gross priceType', () => { let request = spec.buildRequests([bids[5], bids[5]]); let parsedUrl = parseUrl(request.url); diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index 38b36bbaf3d..d69b9e6e3d8 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -98,6 +98,7 @@ describe('AolAdapter', () => { let bidRequest; let logWarnSpy; let formatPixelsStub; + let isOneMobileBidderStub; beforeEach(() => { bidderSettingsBackup = $$PREBID_GLOBAL$$.bidderSettings; @@ -110,13 +111,15 @@ describe('AolAdapter', () => { body: getDefaultBidResponse() }; logWarnSpy = sinon.spy(utils, 'logWarn'); - formatPixelsStub = sinon.stub(spec, '_formatPixels'); + formatPixelsStub = sinon.stub(spec, 'formatPixels'); + isOneMobileBidderStub = sinon.stub(spec, 'isOneMobileBidder'); }); afterEach(() => { $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsBackup; logWarnSpy.restore(); formatPixelsStub.restore(); + isOneMobileBidderStub.restore(); }); it('should return formatted bid response with required properties', () => { @@ -534,10 +537,10 @@ describe('AolAdapter', () => { }); }); - describe('_formatPixels()', () => { + describe('formatPixels()', () => { it('should return pixels wrapped for dropping them once and within nested frames ', () => { let pixels = ''; - let formattedPixels = spec._formatPixels(pixels); + let formattedPixels = spec.formatPixels(pixels); expect(formattedPixels).to.equal( ''); }); - }) + }); + + describe('isOneMobileBidder()', () => { + it('should return false when when bidderCode is not present', () => { + expect(spec.isOneMobileBidder(null)).to.be.false; + }); + + it('should return false for unknown bidder code', () => { + expect(spec.isOneMobileBidder('unknownBidder')).to.be.false; + }); + + it('should return true for aol bidder code', () => { + expect(spec.isOneMobileBidder('aol')).to.be.true; + }); + + it('should return true for one mobile bidder code', () => { + expect(spec.isOneMobileBidder('onemobile')).to.be.true; + }); + }); + + describe('isConsentRequired()', () => { + it('should return false when consentData object is not present', () => { + expect(spec.isConsentRequired(null)).to.be.false; + }); + + it('should return false when gdprApplies equals true and consentString is not present', () => { + let consentData = { + consentString: null, + gdprApplies: true + }; + + expect(spec.isConsentRequired(consentData)).to.be.false; + }); + + it('should return false when consentString is present and gdprApplies equals false', () => { + let consentData = { + consentString: 'consent-string', + gdprApplies: false + }; + + expect(spec.isConsentRequired(consentData)).to.be.false; + }); + + it('should return true when consentString is present and gdprApplies equals true', () => { + let consentData = { + consentString: 'consent-string', + gdprApplies: true + }; + + expect(spec.isConsentRequired(consentData)).to.be.true; + }); + }); + + describe('formatMarketplaceConsentData()', () => { + let consentRequiredStub; + + beforeEach(() => { + consentRequiredStub = sinon.stub(spec, 'isConsentRequired'); + }); + + afterEach(() => { + consentRequiredStub.restore(); + }); + + it('should return empty string when consent is not required', () => { + consentRequiredStub.returns(false); + expect(spec.formatMarketplaceConsentData()).to.be.equal(''); + }); + + it('should return formatted consent data when consent is required', () => { + consentRequiredStub.returns(true); + let formattedConsentData = spec.formatMarketplaceConsentData({ + consentString: 'test-consent' + }); + expect(formattedConsentData).to.be.equal(';euconsent=test-consent;gdpr=1'); + }); + }); + + describe('formatOneMobileDynamicParams()', () => { + let consentRequiredStub; + let secureProtocolStub; + + beforeEach(() => { + consentRequiredStub = sinon.stub(spec, 'isConsentRequired'); + secureProtocolStub = sinon.stub(spec, 'isSecureProtocol'); + }); + + afterEach(() => { + consentRequiredStub.restore(); + secureProtocolStub.restore(); + }); + + it('should return empty string when params are not present', () => { + expect(spec.formatOneMobileDynamicParams()).to.be.equal(''); + }); + + it('should return formatted params when params are present', () => { + let params = { + param1: 'val1', + param2: 'val2', + param3: 'val3' + }; + expect(spec.formatOneMobileDynamicParams(params)).to.contain('¶m1=val1¶m2=val2¶m3=val3'); + }); + + it('should return formatted gdpr params when isConsentRequired returns true', () => { + let consentData = { + consentString: 'test-consent' + }; + consentRequiredStub.returns(true); + expect(spec.formatOneMobileDynamicParams({}, consentData)).to.be.equal('&euconsent=test-consent&gdpr=1'); + }); + + it('should return formatted secure param when isSecureProtocol returns true', () => { + secureProtocolStub.returns(true); + expect(spec.formatOneMobileDynamicParams()).to.be.equal('&secure=1'); + }); + }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 1ba4edfa4ea..abfd50d1746 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -3,8 +3,6 @@ import { spec } from 'modules/appnexusBidAdapter'; import { newBidder } from 'src/adapters/bidderFactory'; import { deepClone } from 'src/utils'; -const adloader = require('../../../src/adloader'); - const ENDPOINT = '//ib.adnxs.com/ut/v3/prebid'; describe('AppNexusAdapter', () => { @@ -173,7 +171,7 @@ describe('AppNexusAdapter', () => { }); }); - it('should attache native params to the request', () => { + it('should attach native params to the request', () => { let bidRequest = Object.assign({}, bidRequests[0], { @@ -290,7 +288,7 @@ describe('AppNexusAdapter', () => { }]); }); - it('should should add payment rules to the request', () => { + it('should add payment rules to the request', () => { let bidRequest = Object.assign({}, bidRequests[0], { @@ -306,21 +304,31 @@ describe('AppNexusAdapter', () => { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); - }) - describe('interpretResponse', () => { - let loadScriptStub; + it('should add gdpr consent information to the request', () => { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + } + }; + bidderRequest.bids = bidRequests; - beforeEach(() => { - loadScriptStub = sinon.stub(adloader, 'loadScript').callsFake((...args) => { - args[1](); - }); - }); + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); - afterEach(() => { - loadScriptStub.restore(); + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; }); + }) + describe('interpretResponse', () => { let response = { 'version': '3.0.0', 'tags': [ diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js new file mode 100644 index 00000000000..5974ac79324 --- /dev/null +++ b/test/spec/modules/consentManagement_spec.js @@ -0,0 +1,292 @@ +import {setConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction} from 'modules/consentManagement'; +import {gdprDataHandler} from 'src/adaptermanager'; +import * as utils from 'src/utils'; +import { config } from 'src/config'; + +let assert = require('chai').assert; +let expect = require('chai').expect; + +describe('consentManagement', function () { + describe('setConfig tests:', () => { + describe('empty setConfig value', () => { + beforeEach(() => { + sinon.stub(utils, 'logInfo'); + }); + + afterEach(() => { + utils.logInfo.restore(); + config.resetConfig(); + }); + + it('should use system default values', () => { + setConfig({}); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + expect(allowAuction).to.be.true; + sinon.assert.callCount(utils.logInfo, 3); + }); + }); + + describe('valid setConfig value', () => { + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + }); + it('results in all user settings overriding system defaults', () => { + let allConfig = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + + setConfig(allConfig); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(7500); + expect(allowAuction).to.be.false; + }); + }); + }); + + describe('requestBidsHook tests:', () => { + let goodConfigWithCancelAuction = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + + let goodConfigWithAllowAuction = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: true + }; + + let didHookReturn; + + afterEach(() => { + gdprDataHandler.consentData = null; + resetConsentData(); + }); + + describe('error checks:', () => { + describe('unknown CMP framework ID:', () => { + beforeEach(() => { + sinon.stub(utils, 'logWarn'); + }); + + afterEach(() => { + utils.logWarn.restore(); + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + gdprDataHandler.consentData = null; + }); + + it('should return Warning message and return to hooked function', () => { + let badCMPConfig = { + cmpApi: 'bad' + }; + setConfig(badCMPConfig); + expect(userCMP).to.be.equal(badCMPConfig.cmpApi); + + didHookReturn = false; + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent).to.be.null; + }); + }); + }); + + describe('already known consentData:', () => { + let cmpStub = sinon.stub(); + + beforeEach(() => { + didHookReturn = false; + window.__cmp = function() {}; + }); + + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + cmpStub.restore(); + delete window.__cmp; + gdprDataHandler.consentData = null; + }); + + it('should bypass CMP and simply use previously stored consentData', () => { + let testConsentData = { + gdprApplies: true, + metadata: 'xyz' + }; + + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + setConfig(goodConfigWithAllowAuction); + requestBidsHook({}, () => {}); + cmpStub.restore(); + + // reset the stub to ensure it wasn't called during the second round of calls + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.metadata); + expect(consent.gdprApplies).to.be.true; + sinon.assert.notCalled(cmpStub); + }); + }); + + describe('CMP workflow for iframed page', () => { + let eventStub = sinon.stub(); + let cmpStub = sinon.stub(); + + beforeEach(() => { + didHookReturn = false; + resetConsentData(); + window.__cmp = function() {}; + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + eventStub.restore(); + cmpStub.restore(); + delete window.__cmp; + utils.logError.restore(); + utils.logWarn.restore(); + gdprDataHandler.consentData = null; + }); + + it('should return the consent string from a postmessage + addEventListener response', () => { + let testConsentData = { + data: { + __cmpReturn: { + returnValue: { + gdprApplies: true, + metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A==' + } + } + } + }; + eventStub = sinon.stub(window, 'addEventListener').callsFake((...args) => { + args[1](testConsentData); + }); + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2]({ + gdprApplies: true, + metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A==' + }); + }); + + setConfig(goodConfigWithAllowAuction); + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal('BOJy+UqOJy+UqABAB+AAAAAZ+A=='); + expect(consent.gdprApplies).to.be.true; + }); + }); + + describe('CMP workflow for normal pages:', () => { + let cmpStub = sinon.stub(); + + beforeEach(() => { + didHookReturn = false; + resetConsentData(); + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + window.__cmp = function() {}; + }); + + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + cmpStub.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + delete window.__cmp; + gdprDataHandler.consentData = null; + }); + + it('performs lookup check and stores consentData for a valid existing user', () => { + let testConsentData = { + gdprApplies: true, + metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A==' + }; + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + setConfig(goodConfigWithAllowAuction); + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.metadata); + expect(consent.gdprApplies).to.be.true; + }); + + it('throws an error when processCmpData check failed while config had allowAuction set to false', () => { + let testConsentData = null; + + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + setConfig(goodConfigWithCancelAuction); + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logError); + expect(didHookReturn).to.be.false; + expect(consent).to.be.null; + }); + + it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', () => { + let testConsentData = null; + + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + setConfig(goodConfigWithAllowAuction); + + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.be.undefined; + expect(consent.gdprApplies).to.be.undefined; + }); + }); + }); +}); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 35146542375..b763c111998 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -2,6 +2,7 @@ import {expect} from 'chai'; import {spec} from 'modules/openxBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory'; import {userSync} from 'src/userSync'; +import * as utils from 'src/utils'; const URLBASE = '/w/1.0/arj'; const URLBASEVIDEO = '/v/1.0/avjp'; @@ -9,6 +10,116 @@ const URLBASEVIDEO = '/v/1.0/avjp'; describe('OpenxAdapter', () => { const adapter = newBidder(spec); + /** + * Type Definitions + */ + + /** + * @typedef {{ + * impression: string, + * inview: string, + * click: string + * }} + */ + let OxArjTracking; + /** + * @typedef {{ + * ads: { + * version: number, + * count: number, + * pixels: string, + * ad: Array + * } + * }} + */ + let OxArjResponse; + /** + * @typedef {{ + * adunitid: number, + * adid:number, + * type: string, + * htmlz: string, + * framed: number, + * is_fallback: number, + * ts: string, + * cpipc: number, + * pub_rev: string, + * tbd: ?string, + * adv_id: string, + * deal_id: string, + * auct_win_is_deal: number, + * brand_id: string, + * currency: string, + * idx: string, + * creative: Array + * }} + */ + let OxArjAdUnit; + /** + * @typedef {{ + * id: string, + * width: string, + * height: string, + * target: string, + * mime: string, + * media: string, + * tracking: OxArjTracking + * }} + */ + let OxArjCreative; + + // HELPER METHODS + /** + * @type {OxArjCreative} + */ + const DEFAULT_TEST_ARJ_CREATIVE = { + id: '0', + width: 'test-width', + height: 'test-height', + target: 'test-target', + mime: 'test-mime', + media: 'test-media', + tracking: { + impression: 'test-impression', + inview: 'test-inview', + click: 'test-click' + } + }; + + /** + * @type {OxArjAdUnit} + */ + const DEFAULT_TEST_ARJ_AD_UNIT = { + adunitid: 0, + type: 'test-type', + html: 'test-html', + framed: 0, + is_fallback: 0, + ts: 'test-ts', + tbd: 'NaN', + deal_id: undefined, + auct_win_is_deal: undefined, + cpipc: 0, + pub_rev: 'test-pub_rev', + adv_id: 'test-adv_id', + brand_id: 'test-brand_id', + currency: 'test-currency', + idx: '0', + creative: [DEFAULT_TEST_ARJ_CREATIVE] + }; + + /** + * @type {OxArjResponse} + */ + const DEFAULT_ARJ_RESPONSE = { + ads: { + version: 0, + count: 1, + pixels: 'http://testpixels.net', + ad: [DEFAULT_TEST_ARJ_AD_UNIT] + } + }; + describe('inherited functions', () => { it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); @@ -53,12 +164,14 @@ describe('OpenxAdapter', () => { bidder: 'openx', params: { unit: '12345678', - delDomain: 'test-del-domain', + delDomain: 'test-del-domain' }, adUnitCode: 'adunit-code', - mediaTypes: {video: { - playerSize: [640, 480] - }}, + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', @@ -302,7 +415,7 @@ describe('OpenxAdapter', () => { }, 'params': { 'unit': '12345678', - 'delDomain': 'test-del-domain', + 'delDomain': 'test-del-domain' }, 'adUnitCode': 'adunit-code', 'bidId': '30b31c1838de1e', @@ -384,101 +497,275 @@ describe('OpenxAdapter', () => { userSync.registerSync.restore(); }); - const bids = [{ - 'bidder': 'openx', - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'mediaType': 'banner', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - const bidRequest = { - method: 'GET', - url: '//openx-d.openx.net/v/1.0/arj', - data: {}, - payload: {'bids': bids, 'startTime': new Date()} - }; - - const bidResponse = { - 'ads': - { - 'version': 1, - 'count': 1, - 'pixels': 'http://testpixels.net', - 'ad': [ - { - 'adunitid': 12345678, - 'adid': 5678, - 'type': 'html', - 'html': 'test_html', - 'framed': 1, - 'is_fallback': 0, - 'ts': 'ts', - 'cpipc': 1000, - 'pub_rev': '1000', - 'adv_id': 'adv_id', - 'brand_id': '', - 'creative': [ - { - 'width': '300', - 'height': '250', - 'target': '_blank', - 'mime': 'text/html', - 'media': 'test_media', - 'tracking': { - 'impression': 'http://openx-d.openx.net/v/1.0/ri?ts=ts', - 'inview': 'test_inview', - 'click': 'test_click' - } - } - ] - }] + describe('when there is a standard response', function () { + const creativeOverride = { + id: 234, + width: '300', + height: '250', + tracking: { + impression: 'http://openx-d.openx.net/v/1.0/ri?ts=ts' } + }; - }; - it('should return correct bid response', () => { - const expectedResponse = [ - { - 'requestId': '30b31c1838de1e', - 'cpm': 1, - 'width': '300', - 'height': '250', - 'creativeId': 5678, - 'ad': 'test_html', - 'ttl': 300, - 'netRevenue': true, - 'currency': 'USD', - 'ts': 'ts' - } - ]; + const adUnitOverride = { + ts: 'test-1234567890-ts', + idx: '0', + currency: 'USD', + pub_rev: '10000', + html: '
OpenX Ad
' + }; + let adUnit; + let bidResponse; + + let bid; + let bidRequest; + let bidRequestConfigs; + + beforeEach(function () { + bidRequestConfigs = [{ + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + + bidRequest = { + method: 'GET', + url: '//openx-d.openx.net/v/1.0/arj', + data: {}, + payload: {'bids': bidRequestConfigs, 'startTime': new Date()} + }; + + adUnit = mockAdUnit(adUnitOverride, creativeOverride); + bidResponse = mockArjResponse(undefined, [adUnit]); + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + }); + + it('should return a price', function () { + expect(bid.cpm).to.equal(parseInt(adUnitOverride.pub_rev, 10) / 1000); + }); + + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidRequest.payload.bids[0].bidId); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(creativeOverride.width); + expect(bid.height).to.equal(creativeOverride.height); + }); + + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(creativeOverride.id); + }); + + it('should return an ad', function () { + expect(bid.ad).to.equal(adUnitOverride.html); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); - const result = spec.interpretResponse({body: bidResponse}, bidRequest); - expect(Object.keys(result[0])).to.eql(Object.keys(expectedResponse[0])); + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + + it('should return a currency', function () { + expect(bid.currency).to.equal(adUnitOverride.currency); + }); + + it('should return a transaction state', function () { + expect(bid.ts).to.equal(adUnitOverride.ts); + }); + + it('should register a beacon', () => { + spec.interpretResponse({body: bidResponse}, bidRequest); + sinon.assert.calledWith(userSync.registerSync, 'image', 'openx', sinon.match(new RegExp(`\/\/openx-d\.openx\.net.*\/bo\?.*ts=${adUnitOverride.ts}`))); + }); }); - it('handles nobid responses', () => { - const bidResponse = { - 'ads': - { - 'version': 1, - 'count': 1, - 'pixels': 'http://testpixels.net', - 'ad': [] - } + describe('when there is a deal', function () { + const adUnitOverride = { + deal_id: 'ox-1000' }; + let adUnit; + let bidResponse; - const result = spec.interpretResponse({body: bidResponse}, bidRequest); - expect(result.length).to.equal(0); + let bid; + let bidRequestConfigs; + let bidRequest; + + beforeEach(function () { + bidRequestConfigs = [{ + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + + bidRequest = { + method: 'GET', + url: '//openx-d.openx.net/v/1.0/arj', + data: {}, + payload: {'bids': bidRequestConfigs, 'startTime': new Date()} + }; + adUnit = mockAdUnit(adUnitOverride); + bidResponse = mockArjResponse(null, [adUnit]); + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + mockArjResponse(); + }); + + it('should return a deal id', function () { + expect(bid.dealId).to.equal(adUnitOverride.deal_id); + }); }); - it('should register a beacon', () => { - spec.interpretResponse({body: bidResponse}, bidRequest); - sinon.assert.calledWith(userSync.registerSync, 'image', 'openx', sinon.match(/\/\/openx-d\.openx\.net.*\/bo\?.*ts=ts/)); + describe('when there is no bids in the response', function () { + let bidRequest; + let bidRequestConfigs; + + beforeEach(function () { + bidRequestConfigs = [{ + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + + bidRequest = { + method: 'GET', + url: '//openx-d.openx.net/v/1.0/arj', + data: {}, + payload: {'bids': bidRequestConfigs, 'startTime': new Date()} + }; + }); + + it('handles nobid responses', () => { + const bidResponse = { + 'ads': + { + 'version': 1, + 'count': 1, + 'pixels': 'http://testpixels.net', + 'ad': [] + } + }; + + const result = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(result.length).to.equal(0); + }); + }); + + describe('when adunits return out of order', function () { + const bidRequests = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[100, 111]] + } + }, + bidId: 'test-bid-request-id-1', + bidderRequestId: 'test-request-1', + auctionId: 'test-auction-id-1' + }, { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[200, 222]] + } + }, + bidId: 'test-bid-request-id-2', + bidderRequestId: 'test-request-1', + auctionId: 'test-auction-id-1' + }, { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 333]] + } + }, + 'bidId': 'test-bid-request-id-3', + 'bidderRequestId': 'test-request-1', + 'auctionId': 'test-auction-id-1' + }]; + const bidRequest = { + method: 'GET', + url: '//openx-d.openx.net/v/1.0/arj', + data: {}, + payload: {'bids': bidRequests, 'startTime': new Date()} + }; + + let outOfOrderAdunits = [ + mockAdUnit({ + idx: '1' + }, { + width: bidRequests[1].mediaTypes.banner.sizes[0][0], + height: bidRequests[1].mediaTypes.banner.sizes[0][1] + }), + mockAdUnit({ + idx: '2' + }, { + width: bidRequests[2].mediaTypes.banner.sizes[0][0], + height: bidRequests[2].mediaTypes.banner.sizes[0][1] + }), + mockAdUnit({ + idx: '0' + }, { + width: bidRequests[0].mediaTypes.banner.sizes[0][0], + height: bidRequests[0].mediaTypes.banner.sizes[0][1] + }) + ]; + + let bidResponse = mockArjResponse(undefined, outOfOrderAdunits); + + it('should return map adunits back to the proper request', function () { + const bids = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(bids[0].requestId).to.equal(bidRequests[1].bidId); + expect(bids[0].width).to.equal(bidRequests[1].mediaTypes.banner.sizes[0][0]); + expect(bids[0].height).to.equal(bidRequests[1].mediaTypes.banner.sizes[0][1]); + expect(bids[1].requestId).to.equal(bidRequests[2].bidId); + expect(bids[1].width).to.equal(bidRequests[2].mediaTypes.banner.sizes[0][0]); + expect(bids[1].height).to.equal(bidRequests[2].mediaTypes.banner.sizes[0][1]); + expect(bids[2].requestId).to.equal(bidRequests[0].bidId); + expect(bids[2].width).to.equal(bidRequests[0].mediaTypes.banner.sizes[0][0]); + expect(bids[2].height).to.equal(bidRequests[0].mediaTypes.banner.sizes[0][1]); + }); }); }); @@ -615,18 +902,88 @@ describe('OpenxAdapter', () => { it('should register the pixel iframe from video ad response', () => { let syncs = spec.getUserSyncs( - { iframeEnabled: true }, - [{ body: { pixels: syncUrl } }] + {iframeEnabled: true}, + [{body: {pixels: syncUrl}}] ); - expect(syncs).to.deep.equal([{ type: 'iframe', url: syncUrl }]); + expect(syncs).to.deep.equal([{type: 'iframe', url: syncUrl}]); }); it('should register the default iframe if no pixels available', () => { let syncs = spec.getUserSyncs( - { iframeEnabled: true }, + {iframeEnabled: true}, [] ); - expect(syncs).to.deep.equal([{ type: 'iframe', url: '//u.openx.net/w/1.0/pd' }]); + expect(syncs).to.deep.equal([{type: 'iframe', url: '//u.openx.net/w/1.0/pd'}]); }); }); + + /** + * Makes sure the override object does not introduce + * new fields against the contract + * + * This does a shallow check in order to make key checking simple + * with respect to what a helper handles. For helpers that have + * nested fields, either check your design on maybe breaking it up + * to smaller, manageable pieces + * + * OR just call this on your nth level field if necessary. + * + * @param {Object} override Object with keys that overrides the default + * @param {Object} contract Original object contains the default fields + * @param {string} typeName Name of the type we're checking for error messages + * @throws {AssertionError} + */ + function overrideKeyCheck(override, contract, typeName) { + expect(contract).to.include.all.keys(Object.keys(override)); + } + + /** + * Creates a mock ArjResponse + * @param {OxArjResponse=} response + * @param {Array=} adUnits + * @throws {AssertionError} + * @return {OxArjResponse} + */ + function mockArjResponse(response, adUnits = []) { + let mockedArjResponse = utils.deepClone(DEFAULT_ARJ_RESPONSE); + + if (response) { + overrideKeyCheck(response, DEFAULT_ARJ_RESPONSE, 'OxArjResponse'); + overrideKeyCheck(response.ads, DEFAULT_ARJ_RESPONSE.ads, 'OxArjResponse'); + Object.assign(mockedArjResponse, response); + } + + if (adUnits.length) { + mockedArjResponse.ads.count = adUnits.length; + mockedArjResponse.ads.ad = adUnits.map((adUnit, index) => { + overrideKeyCheck(adUnit, DEFAULT_TEST_ARJ_AD_UNIT, 'OxArjAdUnit'); + return Object.assign(utils.deepClone(DEFAULT_TEST_ARJ_AD_UNIT), adUnit); + }); + } + + return mockedArjResponse; + } + + /** + * Creates a mock ArjAdUnit + * @param {OxArjAdUnit=} adUnit + * @param {OxArjCreative=} creative + * @throws {AssertionError} + * @return {OxArjAdUnit} + */ + function mockAdUnit(adUnit, creative) { + overrideKeyCheck(adUnit, DEFAULT_TEST_ARJ_AD_UNIT, 'OxArjAdUnit'); + + let mockedAdUnit = Object.assign(utils.deepClone(DEFAULT_TEST_ARJ_AD_UNIT), adUnit); + + if (creative) { + overrideKeyCheck(creative, DEFAULT_TEST_ARJ_CREATIVE); + if (creative.tracking) { + overrideKeyCheck(creative.tracking, DEFAULT_TEST_ARJ_CREATIVE.tracking, 'OxArjCreative'); + } + Object.assign(mockedAdUnit.creative[0], creative); + } + + return mockedAdUnit; + } }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index eb14c300e33..cdb3113c205 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,6 +6,7 @@ import cookie from 'src/cookie'; import { userSync } from 'src/userSync'; import { ajax } from 'src/ajax'; import { config } from 'src/config'; +import { requestBidsHook } from 'modules/consentManagement'; let CONFIG = { accountId: '1', @@ -391,6 +392,38 @@ describe('S2S Adapter', () => { expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string'); }); + it('adds gdpr consent information to ortb2 request depending on module use', () => { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + + let consentConfig = { consentManagement: { cmp: 'iab' }, s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123', + gdprApplies: true + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.regs.ext.gdpr).is.equal(1); + expect(requestBid.user.ext.consent).is.equal('abc123'); + + config.resetConfig(); + config.setConfig({s2sConfig: CONFIG}); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(requests[1].requestBody); + + expect(requestBid.regs).to.not.exist; + expect(requestBid.user).to.not.exist; + + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + }); + it('sets invalid cacheMarkup value to 0', () => { const s2sConfig = Object.assign({}, CONFIG, { cacheMarkup: 999 diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index cbf17f9478a..7ea10315a4e 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -44,7 +44,10 @@ describe('PubMatic adapter', () => { 'price': 1.3, 'adm': 'image3.pubmatic.com Layer based creative', 'h': 250, - 'w': 300 + 'w': 300, + 'ext': { + 'deal_channel': 6 + } }] }] } @@ -136,8 +139,44 @@ describe('PubMatic adapter', () => { expect(data.ext.wrapper.wv).to.equal(constants.REPO_AND_VERSION); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(bidRequests[0].params.profId); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(bidRequests[0].params.verId); // OpenWrap: Wrapper Profile Version ID + expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID + expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID + + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor + expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid + }); + + it('Request params check with GDPR Consent', () => { + let bidRequest = { + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.user.ext.consent).to.equal('kjfdniwjnifwenrif3'); + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.at).to.equal(1); // auction type + expect(data.cur[0]).to.equal('USD'); // currency + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB + expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender + expect(data.device.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude + expect(data.device.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.user.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude + expect(data.user.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.ext.wrapper.wv).to.equal(constants.REPO_AND_VERSION); // Wrapper Version + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID + expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID + expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor @@ -175,6 +214,24 @@ describe('PubMatic adapter', () => { expect(response[0].referrer).to.include(utils.getTopWindowUrl()); expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm); }); + + it('should check for dealChannel value selection', () => { + let request = spec.buildRequests(bidRequests); + let response = spec.interpretResponse(bidResponses, request); + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].dealChannel).to.equal('PMPG'); + }); + + it('should check for unexpected dealChannel value selection', () => { + let request = spec.buildRequests(bidRequests); + let updateBiResponse = bidResponses; + updateBiResponse.body.seatbid[0].bid[0].ext.deal_channel = 11; + + let response = spec.interpretResponse(updateBiResponse, request); + + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].dealChannel).to.equal(null); + }); }); }); }); diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index b4793256ee0..709dbeb76a2 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -273,4 +273,25 @@ describe('PulsePoint Adapter Tests', () => { expect(ortbRequest.app.storeurl).to.equal('http://pulsepoint.com/apps'); expect(ortbRequest.app.domain).to.equal('pulsepoint.com'); }); + + it('Verify GDPR', () => { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + } + }; + const request = spec.buildRequests(slotConfigs, bidderRequest); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // user object + expect(ortbRequest.user).to.not.equal(null); + expect(ortbRequest.user.ext).to.not.equal(null); + expect(ortbRequest.user.ext.consent).to.equal('serialized_gpdr_data'); + // regs object + expect(ortbRequest.regs).to.not.equal(null); + expect(ortbRequest.regs.ext).to.not.equal(null); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + }); }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index e3ffef9997e..e6f4d873ca5 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -72,6 +72,7 @@ describe('the rubicon adapter', () => { 'p_aso.video.ext.skipdelay': '15' } }; + bid.params.secure = false; } function createVideoBidderRequestNoVideo() { @@ -274,7 +275,7 @@ describe('the rubicon adapter', () => { bidderRequest.bids[0].params.secure = true; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).rf).to.equal('https://www.rubiconproject.com'); + expect(parseQuery(request.data).rf).to.equal('http://www.rubiconproject.com'); }); it('should use rubicon sizes if present (including non-mappable sizes)', () => { @@ -608,6 +609,7 @@ describe('the rubicon adapter', () => { expect(post).to.have.property('timeout').that.is.a('number'); expect(post.timeout < 5000).to.equal(true); expect(post.stash_creatives).to.equal(true); + expect(post.rp_secure).to.equal(false); expect(post.gdpr_consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); expect(post.gdpr).to.equal(1); @@ -671,6 +673,7 @@ describe('the rubicon adapter', () => { expect(post).to.have.property('timeout').that.is.a('number'); expect(post.timeout < 5000).to.equal(true); expect(post.stash_creatives).to.equal(true); + expect(post.rp_secure).to.equal(true); expect(post.gdpr_consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); expect(post.gdpr).to.equal(1); diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index a440b3d43c4..b19b79c7886 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -82,7 +82,30 @@ describe('sovrnBidAdapter', function() { const request = spec.buildRequests(ivBidRequests); expect(request.data).to.contain('"iv":"vet"') - }) + }); + + it('sends gdpr info if exists', () => { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'sovrn', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.regs.ext.gdpr).to.exist.and.to.be.a('number'); + expect(payload.regs.ext.gdpr).to.equal(1); + expect(payload.user.ext.consent).to.exist.and.to.be.a('string'); + expect(payload.user.ext.consent).to.equal(consentString); + }); it('converts tagid to string', () => { const ivBidRequests = [{ @@ -106,22 +129,26 @@ describe('sovrnBidAdapter', function() { }); describe('interpretResponse', () => { - let response = { - body: { - 'id': '37386aade21a71', - 'seatbid': [{ - 'bid': [{ - 'id': 'a_403370_332fdb9b064040ddbec05891bd13ab28', - 'impid': '263c448586f5a1', - 'price': 0.45882675, - 'nurl': '', - 'adm': '', - 'h': 90, - 'w': 728 + let response; + beforeEach(() => { + response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'crid': 'creativelycreatedcreativecreative', + 'impid': '263c448586f5a1', + 'price': 0.45882675, + 'nurl': '', + 'adm': '', + 'h': 90, + 'w': 728 + }] }] - }] - } - }; + } + }; + }); it('should get the correct bid response', () => { let expectedResponse = [{ @@ -129,7 +156,27 @@ describe('sovrnBidAdapter', function() { 'cpm': 0.45882675, 'width': 728, 'height': 90, - 'creativeId': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(`>`), + 'ttl': 60000 + }]; + + let result = spec.interpretResponse(response); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); + + it('crid should default to the bid id if not on the response', () => { + delete response.body.seatbid[0].bid[0].crid; + let expectedResponse = [{ + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': response.body.seatbid[0].bid[0].id, 'dealId': null, 'currency': 'USD', 'netRevenue': true, @@ -150,7 +197,7 @@ describe('sovrnBidAdapter', function() { 'cpm': 0.45882675, 'width': 728, 'height': 90, - 'creativeId': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'creativeId': 'creativelycreatedcreativecreative', 'dealId': 'baking', 'currency': 'USD', 'netRevenue': true, diff --git a/test/spec/sizeMapping_spec.js b/test/spec/sizeMapping_spec.js index 74b86a8c5aa..ecf276e6618 100644 --- a/test/spec/sizeMapping_spec.js +++ b/test/spec/sizeMapping_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { resolveStatus, setSizeConfig } from 'src/sizeMapping'; +import { resolveStatus, setSizeConfig, resolveBidOverrideSizes } from 'src/sizeMapping'; import includes from 'core-js/library/fn/array/includes'; let utils = require('src/utils'); @@ -8,6 +8,11 @@ let deepClone = utils.deepClone; describe('sizeMapping', () => { var testSizes = [[970, 90], [728, 90], [300, 250], [300, 100], [80, 80]]; + // Should also handle sizes defined with Object format IE { w: 300, h: 250 } + const testSizesDefinedWithObjs = [ + { w: 728, h: 90 }, { w: 300, h: 250 }, { w: 300, h: 100 } + ] + var sizeConfig = [{ 'mediaQuery': '(min-width: 1200px)', 'sizesSupported': [ @@ -135,6 +140,17 @@ describe('sizeMapping', () => { sizes: testSizes }) }); + + it('when adUnit.sizes are defined using Objects, it should still filter sizes for matching mediaQuery block', () => { + matchMediaOverride = (str) => str === '(min-width: 1200px)' ? {matches: true} : {matches: false}; + + let statusObjFormatSizes = resolveStatus(undefined, testSizesDefinedWithObjs, sizeConfig); + + expect(statusObjFormatSizes).to.deep.equal({ + active: true, + sizes: [{ w: 728, h: 90 }, { w: 300, h: 250 }] + }) + }); }); describe('when handling labels', () => { @@ -206,4 +222,52 @@ describe('sizeMapping', () => { }); }); }); + + describe('when handling sizes defined on a bid', () => { + it('should filter an intersection of adUnit sizes and bid sizes', () => { + const testAdUnitSizes = deepClone(testSizes); + + const bidOneSize = {sizes: [[728, 90]]}; + const bidMultipleSizes = {sizes: [[728, 90], [300, 250], [300, 100]]}; + + // Test single valid bid size, should return single valid size + expect(resolveBidOverrideSizes(bidOneSize, testAdUnitSizes)).to.deep.equal(bidOneSize.sizes); + // Test multiple valid bid sizes, should return valid sizes + expect(resolveBidOverrideSizes(bidMultipleSizes, testAdUnitSizes)).to.deep.equal(bidMultipleSizes.sizes); + }); + + it('should filter bid sizes with adUnit sizes defined as a list of objects', () => { + // adUnit sizes defined using w/h object structure Array.<{ w:number, h:number }> + const testAdUnitSizesObjs = deepClone(testSizes).map(size => ({w: size[0], h: size[1]})); + + const bidOneSize = {sizes: [[728, 90]]}; + const bidMultipleSizes = {sizes: [[728, 90], [300, 250], [300, 100]]}; + + // Test single valid bid size, should return single valid size but defined as Array.<{ w:number, h:number }> + expect(resolveBidOverrideSizes(bidOneSize, testAdUnitSizesObjs, true)).to.deep.equal(bidOneSize.sizes.map(size => ({w: size[0], h: size[1]}))); + // Test multiple valid bid sizes, should return valid sizes but defined as Array.<{ w:number, h:number }> + expect(resolveBidOverrideSizes(bidMultipleSizes, testAdUnitSizesObjs, true)).to.deep.equal(bidMultipleSizes.sizes.map(size => ({w: size[0], h: size[1]}))); + }); + + it('should return unfiltered sizes when bid sizes are invalid', () => { + sandbox.stub(utils, 'logWarn'); + const testAdUnitSizes = deepClone(testSizes); + const bidInvalidSize = {sizes: [[728, 250]]}; + + // Invalid bid sizes, should return unfiltered sizes and log warning + expect(resolveBidOverrideSizes(bidInvalidSize, testAdUnitSizes)).to.deep.equal(testAdUnitSizes); + expect(utils.logWarn.firstCall.args[0]).to.match(/Invalid bid override sizes/); + }); + + it('should return unfiltered sizes when bid sizes or adUnit sizes are empty', () => { + const testAdUnitSizes = deepClone(testSizes); + const bidOneSize = {sizes: [[728, 90]]}; + const bidEmptySizes = {sizes: []}; + + // Empty bid sizes, should return unfiltered bids + expect(resolveBidOverrideSizes(bidEmptySizes, testAdUnitSizes)).to.deep.equal(testAdUnitSizes); + // Valid bid size and empty adUnit sizes, should return empty adUnit sizes + expect(resolveBidOverrideSizes(bidOneSize, [])).to.deep.equal([]); + }); + }) }); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 39e468d4959..8b1c164a804 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -716,141 +716,171 @@ describe('adapterManager tests', () => { expect(AdapterManager.videoAdapters).to.include(alias); }); }); + }); + + describe('makeBidRequests', () => { + let adUnits; + beforeEach(() => { + adUnits = utils.deepClone(getAdUnits()).map(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder)); + return adUnit; + }) + }); - describe('makeBidRequests', () => { - let adUnits; + describe('setBidderSequence', () => { beforeEach(() => { - adUnits = utils.deepClone(getAdUnits()).map(adUnit => { - adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder)); - return adUnit; - }) + sinon.spy(utils, 'shuffle'); }); - describe('setBidderSequence', () => { - beforeEach(() => { - sinon.spy(utils, 'shuffle'); - }); - - afterEach(() => { - config.resetConfig(); - utils.shuffle.restore(); - }); + afterEach(() => { + config.resetConfig(); + utils.shuffle.restore(); + }); - it('setting to `random` uses shuffled order of adUnits', () => { - config.setConfig({ bidderSequence: 'random' }); - let bidRequests = AdapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - sinon.assert.calledOnce(utils.shuffle); - }); + it('setting to `random` uses shuffled order of adUnits', () => { + config.setConfig({ bidderSequence: 'random' }); + let bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + sinon.assert.calledOnce(utils.shuffle); }); + }); - describe('sizeMapping', () => { - beforeEach(() => { - sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true})); - }); + describe('sizeMapping', () => { + beforeEach(() => { + sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true})); + }); - afterEach(() => { - matchMedia.restore(); - setSizeConfig([]); - }); + afterEach(() => { + matchMedia.restore(); + setSizeConfig([]); + }); - it('should not filter bids w/ no labels', () => { - let bidRequests = AdapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests.length).to.equal(2); - let rubiconBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'rubicon'); - expect(rubiconBidRequests.bids.length).to.equal(1); - expect(rubiconBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === rubiconBidRequests.bids[0].adUnitCode).sizes); - - let appnexusBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'appnexus'); - expect(appnexusBidRequests.bids.length).to.equal(2); - expect(appnexusBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[0].adUnitCode).sizes); - expect(appnexusBidRequests.bids[1].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[1].adUnitCode).sizes); - }); + it('should not filter bids w/ no labels', () => { + let bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests.length).to.equal(2); + let rubiconBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'rubicon'); + expect(rubiconBidRequests.bids.length).to.equal(1); + expect(rubiconBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === rubiconBidRequests.bids[0].adUnitCode).sizes); + + let appnexusBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'appnexus'); + expect(appnexusBidRequests.bids.length).to.equal(2); + expect(appnexusBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[0].adUnitCode).sizes); + expect(appnexusBidRequests.bids[1].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[1].adUnitCode).sizes); + }); - it('should filter sizes using size config', () => { - let validSizes = [ - [728, 90], - [300, 250] - ]; - - let validSizeMap = validSizes.map(size => size.toString()).reduce((map, size) => { - map[size] = true; - return map; - }, {}); - - setSizeConfig([{ - 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)', - 'sizesSupported': validSizes, - 'labels': ['tablet', 'phone'] - }]); - - let bidRequests = AdapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); + it('should filter sizes using size config', () => { + let validSizes = [ + [728, 90], + [300, 250] + ]; + + let validSizeMap = validSizes.map(size => size.toString()).reduce((map, size) => { + map[size] = true; + return map; + }, {}); + + setSizeConfig([{ + 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)', + 'sizesSupported': validSizes, + 'labels': ['tablet', 'phone'] + }]); + + let bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); // only valid sizes as specified in size config should show up in bidRequests - bidRequests.forEach(bidRequest => { - bidRequest.bids.forEach(bid => { - bid.sizes.forEach(size => { - expect(validSizeMap[size]).to.equal(true); - }); + bidRequests.forEach(bidRequest => { + bidRequest.bids.forEach(bid => { + bid.sizes.forEach(size => { + expect(validSizeMap[size]).to.equal(true); }); }); - - setSizeConfig([{ - 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)', - 'sizesSupported': [], - 'labels': ['tablet', 'phone'] - }]); - - bidRequests = AdapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - // if no valid sizes, all bidders should be filtered out - expect(bidRequests.length).to.equal(0); }); - it('should filter adUnits/bidders based on applied labels', () => { - adUnits[0].labelAll = ['visitor-uk', 'mobile']; - adUnits[1].labelAny = ['visitor-uk', 'desktop']; - adUnits[1].bids[0].labelAny = ['mobile']; - adUnits[1].bids[1].labelAll = ['desktop']; + setSizeConfig([{ + 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)', + 'sizesSupported': [], + 'labels': ['tablet', 'phone'] + }]); + + bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + // if no valid sizes, all bidders should be filtered out + expect(bidRequests.length).to.equal(0); + }); + + it('should filter adUnits/bidders based on applied labels', () => { + adUnits[0].labelAll = ['visitor-uk', 'mobile']; + adUnits[1].labelAny = ['visitor-uk', 'desktop']; + adUnits[1].bids[0].labelAny = ['mobile']; + adUnits[1].bids[1].labelAll = ['desktop']; - let bidRequests = AdapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - ['visitor-uk', 'desktop'] - ); + let bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + ['visitor-uk', 'desktop'] + ); // only one adUnit and one bid from that adUnit should make it through the applied labels above - expect(bidRequests.length).to.equal(1); - expect(bidRequests[0].bidderCode).to.equal('rubicon'); - expect(bidRequests[0].bids.length).to.equal(1); - expect(bidRequests[0].bids[0].adUnitCode).to.equal(adUnits[1].code); + expect(bidRequests.length).to.equal(1); + expect(bidRequests[0].bidderCode).to.equal('rubicon'); + expect(bidRequests[0].bids.length).to.equal(1); + expect(bidRequests[0].bids[0].adUnitCode).to.equal(adUnits[1].code); + }); + }); + + describe('gdpr consent module', () => { + it('inserts gdprConsent object to bidRequest only when module was enabled', () => { + AdapterManager.gdprDataHandler.setConsentData({ + consentString: 'abc123def456', + consentRequired: true }); + + let bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + expect(bidRequests[0].gdprConsent.consentString).to.equal('abc123def456'); + expect(bidRequests[0].gdprConsent.consentRequired).to.be.true; + + AdapterManager.gdprDataHandler.setConsentData(null); + + bidRequests = AdapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + expect(bidRequests[0].gdprConsent).to.be.undefined; }); }); }); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index f86840dbdba..9218409c46c 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -359,6 +359,33 @@ describe('Utils', function () { }); }); + describe('isPlainObject', function () { + it('should return false with input string', function () { + var output = utils.isPlainObject(obj_string); + assert.deepEqual(output, false); + }); + + it('should return false with input number', function () { + var output = utils.isPlainObject(obj_number); + assert.deepEqual(output, false); + }); + + it('should return true with input object', function () { + var output = utils.isPlainObject(obj_object); + assert.deepEqual(output, true); + }); + + it('should return false with input array', function () { + var output = utils.isPlainObject(obj_array); + assert.deepEqual(output, false); + }); + + it('should return false with input function', function () { + var output = utils.isPlainObject(obj_function); + assert.deepEqual(output, false); + }); + }); + describe('isEmpty', function () { it('should return true with empty object', function () { var output = utils.isEmpty(obj_object);