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);