diff --git a/babelConfig.js b/babelConfig.js index 785c6171c42..2592d8dc776 100644 --- a/babelConfig.js +++ b/babelConfig.js @@ -24,7 +24,7 @@ module.exports = function (options = {}) { ], 'plugins': [ [path.resolve(__dirname, './plugins/pbjsGlobals.js'), options], - useLocal('babel-plugin-transform-object-assign'), + [useLocal('@babel/plugin-transform-runtime')], ], } } diff --git a/integrationExamples/gpt/prebidServer_native_example.html b/integrationExamples/gpt/prebidServer_native_example.html index 16c7d38a427..c590f0bcee5 100644 --- a/integrationExamples/gpt/prebidServer_native_example.html +++ b/integrationExamples/gpt/prebidServer_native_example.html @@ -133,8 +133,8 @@ */ -export function fireNativeTrackers(message, adObject) { - let trackers; +export function fireNativeTrackers(message, bidResponse) { + const nativeResponse = bidResponse.native.ortb || legacyPropertiesToOrtbNative(bidResponse.native); + if (message.action === 'click') { - trackers = adObject['native'] && adObject['native'].clickTrackers; + fireClickTrackers(nativeResponse); } else { - trackers = adObject['native'] && adObject['native'].impressionTrackers; + fireImpressionTrackers(nativeResponse); + } + return message.action; +} + +export function fireImpressionTrackers(nativeResponse, {runMarkup = (mkup) => insertHtmlIntoIframe(mkup), fetchURL = triggerPixel} = {}) { + const impTrackers = (nativeResponse.eventtrackers || []) + .filter(tracker => tracker.event === TRACKER_EVENTS.impression); - if (adObject['native'] && adObject['native'].javascriptTrackers) { - insertHtmlIntoIframe(adObject['native'].javascriptTrackers); + let {img, js} = impTrackers.reduce((tally, tracker) => { + if (TRACKER_METHODS.hasOwnProperty(tracker.method)) { + tally[TRACKER_METHODS[tracker.method]].push(tracker.url) } + return tally; + }, {img: [], js: []}); + + if (nativeResponse.imptrackers) { + img = img.concat(nativeResponse.imptrackers); } + img.forEach(url => fetchURL(url)); - (trackers || []).forEach(triggerPixel); - return message.action; + js = js.map(url => ``); + if (nativeResponse.jstracker) { + // jstracker is already HTML markup + js = js.concat([nativeResponse.jstracker]); + } + if (js.length) { + runMarkup(js.join('\n')); + } +} + +export function fireClickTrackers(nativeResponse, {fetchURL = triggerPixel} = {}) { + (nativeResponse.link?.clicktrackers || []).forEach(url => fetchURL(url)); } /** @@ -225,32 +393,47 @@ export function getAssetMessage(data, adObject) { return message; } -export function getAllAssetsMessage(data, adObject) { +export function getAllAssetsMessage(data, adObject, {getNativeReq = (bidResponse) => auctionManager.index.getAdUnit(bidResponse).nativeOrtbRequest} = {}) { const message = { message: 'assetResponse', adId: data.adId, - assets: [] }; - Object.keys(adObject.native).forEach(function(key, index) { - if (key === 'adTemplate' && adObject.native[key]) { - message.adTemplate = getAssetValue(adObject.native[key]); - } else if (key === 'rendererUrl' && adObject.native[key]) { - message.rendererUrl = getAssetValue(adObject.native[key]); + // Pass to Prebid Universal Creative all assets, the legacy ones + the ortb ones (under ortb property) + const ortbRequest = getNativeReq(adObject); + let nativeReq = adObject.native; + const ortbResponse = adObject.native?.ortb; + let legacyResponse = {}; + if (ortbRequest && ortbResponse) { + legacyResponse = toLegacyResponse(ortbResponse, ortbRequest); + nativeReq = { + ...adObject.native, + ...legacyResponse + }; + } + if (adObject.native.ortb) { + message.ortb = adObject.native.ortb; + } + message.assets = []; + + Object.keys(nativeReq).forEach(function(key) { + if (key === 'adTemplate' && nativeReq[key]) { + message.adTemplate = getAssetValue(nativeReq[key]); + } else if (key === 'rendererUrl' && nativeReq[key]) { + message.rendererUrl = getAssetValue(nativeReq[key]); } else if (key === 'ext') { - Object.keys(adObject.native[key]).forEach(extKey => { - if (adObject.native[key][extKey]) { - const value = getAssetValue(adObject.native[key][extKey]); + Object.keys(nativeReq[key]).forEach(extKey => { + if (nativeReq[key][extKey]) { + const value = getAssetValue(nativeReq[key][extKey]); message.assets.push({ key: extKey, value }); } }) - } else if (adObject.native[key] && CONSTANTS.NATIVE_KEYS.hasOwnProperty(key)) { - const value = getAssetValue(adObject.native[key]); + } else if (nativeReq[key] && CONSTANTS.NATIVE_KEYS.hasOwnProperty(key)) { + const value = getAssetValue(nativeReq[key]); message.assets.push({ key, value }); } }); - return message; } @@ -280,3 +463,303 @@ function getNativeKeys(adUnit) { ...extraNativeKeys } } + +/** + * converts Prebid legacy native assets request to OpenRTB format + * @param {object} legacyNativeAssets an object that describes a native bid request in Prebid proprietary format + * @returns an OpenRTB format of the same bid request + */ +export function toOrtbNativeRequest(legacyNativeAssets) { + if (!legacyNativeAssets && !isPlainObject(legacyNativeAssets)) { + logError('Native assets object is empty or not an object: ', legacyNativeAssets); + return; + } + const ortb = { + ver: '1.2', + assets: [] + }; + for (let key in legacyNativeAssets) { + // skip conversion for non-asset keys + if (NATIVE_KEYS_THAT_ARE_NOT_ASSETS.includes(key)) continue; + + const asset = legacyNativeAssets[key]; + let required = 0; + if (asset.required && isBoolean(asset.required)) { + required = Number(asset.required); + } + const ortbAsset = { + id: ortb.assets.length, + required + }; + // data cases + if (key in PREBID_NATIVE_DATA_KEYS_TO_ORTB) { + ortbAsset.data = { + type: NATIVE_ASSET_TYPES[PREBID_NATIVE_DATA_KEYS_TO_ORTB[key]] + } + if (asset.len) { + ortbAsset.data.len = asset.len; + } + // icon or image case + } else if (key === 'icon' || key === 'image') { + ortbAsset.img = { + type: key === 'icon' ? NATIVE_IMAGE_TYPES.ICON : NATIVE_IMAGE_TYPES.MAIN, + } + // if min_width and min_height are defined in aspect_ratio, they are preferred + if (asset.aspect_ratios) { + if (!isArray(asset.aspect_ratios)) { + logError("image.aspect_ratios was passed, but it's not a an array:", asset.aspect_ratios); + } else if (!asset.aspect_ratios.length) { + logError("image.aspect_ratios was passed, but it's empty:", asset.aspect_ratios); + } else { + const { min_width: minWidth, min_height: minHeight } = asset.aspect_ratios[0]; + if (!isInteger(minWidth) || !isInteger(minHeight)) { + logError('image.aspect_ratios min_width or min_height are invalid: ', minWidth, minHeight); + } else { + ortbAsset.img.wmin = minWidth; + ortbAsset.img.hmin = minHeight; + } + const aspectRatios = asset.aspect_ratios + .filter((ar) => ar.ratio_width && ar.ratio_height) + .map(ratio => `${ratio.ratio_width}:${ratio.ratio_height}`); + if (aspectRatios.length > 0) { + ortbAsset.img.ext = { + aspectratios: aspectRatios + } + } + } + } + + // if asset.sizes exist, by OpenRTB spec we should remove wmin and hmin + if (asset.sizes) { + if (asset.sizes.length !== 2 || !isInteger(asset.sizes[0]) || !isInteger(asset.sizes[1])) { + logError('image.sizes was passed, but its value is not an array of integers:', asset.sizes); + } else { + ortbAsset.img.w = asset.sizes[0]; + ortbAsset.img.h = asset.sizes[1]; + delete ortbAsset.img.hmin; + delete ortbAsset.img.wmin; + } + } + // title case + } else if (key === 'title') { + ortbAsset.title = { + // in openRTB, len is required for titles, while in legacy prebid was not. + // for this reason, if len is missing in legacy prebid, we're adding a default value of 140. + len: asset.len || 140 + } + // all extensions to the native bid request are passed as is + } else if (key === 'ext') { + ortbAsset.ext = asset; + // in `ext` case, required field is not needed + delete ortbAsset.required; + } + + ortb.assets.push(ortbAsset); + } + return ortb; +} + +/** + * This function converts an OpenRTB native request object to Prebid proprietary + * format. The purpose of this function is to help adapters to handle the + * transition phase where publishers may be using OpenRTB objects but the + * bidder does not yet support it. + * @param {object} openRTBRequest an OpenRTB v1.2 request object + * @returns a Prebid legacy native format request + */ +export function fromOrtbNativeRequest(openRTBRequest) { + if (!isOpenRTBBidRequestValid(openRTBRequest)) { + return; + } + + const oldNativeObject = {}; + for (const asset of openRTBRequest.assets) { + if (asset.title) { + const title = { + required: asset.required ? Boolean(asset.required) : false, + len: asset.title.len + } + oldNativeObject.title = title; + } else if (asset.img) { + const image = { + required: asset.required ? Boolean(asset.required) : false, + } + if (asset.img.w && asset.img.h) { + image.sizes = [asset.img.w, asset.img.h]; + } else if (asset.img.wmin && asset.img.hmin) { + image.aspect_ratios = { + min_width: asset.img.wmin, + min_height: asset.img.hmin, + ratio_width: asset.img.wmin, + ratio_height: asset.img.hmin + } + } + + if (asset.img.type === NATIVE_IMAGE_TYPES.MAIN) { + oldNativeObject.image = image; + } else { + oldNativeObject.icon = image; + } + } else if (asset.data) { + let assetType = Object.keys(NATIVE_ASSET_TYPES).find(k => NATIVE_ASSET_TYPES[k] === asset.data.type); + let prebidAssetName = Object.keys(PREBID_NATIVE_DATA_KEYS_TO_ORTB).find(k => PREBID_NATIVE_DATA_KEYS_TO_ORTB[k] === assetType); + oldNativeObject[prebidAssetName] = { + required: asset.required ? Boolean(asset.required) : false, + } + if (asset.data.len) { + oldNativeObject[prebidAssetName].len = asset.data.len; + } + } + // video was not supported by old prebid assets + } + return oldNativeObject; +} + +/** + * Converts an OpenRTB request to a proprietary Prebid.js format. + * The proprietary Prebid format has many limitations and will be dropped in + * the future; adapters are encouraged to stop using it in favour of OpenRTB format. + * IMPLEMENTATION DETAILS: This function returns the same exact object if no + * conversion is needed. If a conversion is needed (meaning, at least one + * bidRequest contains a native.ortb definition), it will return a copy. + * + * @param {BidRequest[]} bidRequests an array of valid bid requests + * @returns an array of valid bid requests where the openRTB bids are converted to proprietary format. + */ +export function convertOrtbRequestToProprietaryNative(bidRequests) { + if (FEATURES.NATIVE) { + if (!bidRequests || !isArray(bidRequests)) return bidRequests; + // check if a conversion is needed + if (!bidRequests.some(bidRequest => (bidRequest?.mediaTypes || {})[NATIVE]?.ortb)) { + return bidRequests; + } + let bidRequestsCopy = deepClone(bidRequests); + // convert Native ORTB definition to old-style prebid native definition + for (const bidRequest of bidRequestsCopy) { + if (bidRequest.mediaTypes && bidRequest.mediaTypes[NATIVE] && bidRequest.mediaTypes[NATIVE].ortb) { + bidRequest.mediaTypes[NATIVE] = Object.assign( + pick(bidRequest.mediaTypes[NATIVE], NATIVE_KEYS_THAT_ARE_NOT_ASSETS), + fromOrtbNativeRequest(bidRequest.mediaTypes[NATIVE].ortb) + ); + bidRequest.nativeParams = processNativeAdUnitParams(bidRequest.mediaTypes[NATIVE]); + } + } + return bidRequestsCopy; + } + return bidRequests; +} + +/** + * convert PBJS proprietary native properties that are *not* assets to the ORTB native format. + * + * @param legacyNative `bidResponse.native` object as returned by adapters + */ +export function legacyPropertiesToOrtbNative(legacyNative) { + const response = { + link: {}, + eventtrackers: [] + } + Object.entries(legacyNative).forEach(([key, value]) => { + switch (key) { + case 'clickUrl': + response.link.url = value; + break; + case 'clickTrackers': + response.link.clicktrackers = Array.isArray(value) ? value : [value]; + break; + case 'impressionTrackers': + (Array.isArray(value) ? value : [value]).forEach(url => { + response.eventtrackers.push({ + event: TRACKER_EVENTS.impression, + method: TRACKER_METHODS.img, + url + }); + }); + break; + case 'javascriptTrackers': + // jstracker is deprecated, but we need to use it here since 'javascriptTrackers' is markup, not an url + // TODO: at the time of writing this, core expected javascriptTrackers to be a string (despite the name), + // but many adapters are passing an array. It's possible that some of them are, in fact, passing URLs and not markup + // in general, native trackers seem to be neglected and/or broken + response.jstracker = Array.isArray(value) ? value.join('') : value; + break; + } + }) + return response; +} + +export function toOrtbNativeResponse(legacyResponse, ortbRequest) { + // copy the request, so we don't pollute it with response data below + ortbRequest = deepClone(ortbRequest); + + const ortbResponse = { + ...legacyPropertiesToOrtbNative(legacyResponse), + assets: [] + }; + Object.keys(legacyResponse).filter(key => !!legacyResponse[key]).forEach(key => { + const value = legacyResponse[key]; + switch (key) { + // process titles + case 'title': + const titleAsset = ortbRequest.assets.find(asset => asset.title != null); + titleAsset.title = { + text: value + }; + ortbResponse.assets.push(titleAsset); + break; + case 'image': + case 'icon': + const imageType = key === 'image' ? NATIVE_IMAGE_TYPES.MAIN : NATIVE_IMAGE_TYPES.ICON; + const imageAsset = ortbRequest.assets.find(asset => asset.img != null && asset.img.type == imageType); + imageAsset.img = { + url: value + }; + ortbResponse.assets.push(imageAsset); + break; + default: + if (key in PREBID_NATIVE_DATA_KEYS_TO_ORTB) { + const dataAsset = ortbRequest.assets.find(asset => asset.data != null && asset.data.type === NATIVE_ASSET_TYPES[PREBID_NATIVE_DATA_KEYS_TO_ORTB[key]]); + dataAsset.data = { + value + }; + ortbResponse.assets.push(dataAsset); + } + break; + } + }); + return ortbResponse; +} + +/** + * Generates a legacy response from an ortb response. Useful during the transition period. + * @param {*} ortbResponse a standard ortb response object + * @param {*} ortbRequest the ortb request, useful to match ids. + * @returns an object containing the response in legacy native format: { title: "this is a title", image: ... } + */ +function toLegacyResponse(ortbResponse, ortbRequest) { + const legacyResponse = {}; + const requestAssets = ortbRequest?.assets || []; + legacyResponse.clickUrl = ortbResponse.link.url; + for (const asset of ortbResponse?.assets || []) { + const requestAsset = requestAssets.find(reqAsset => asset.id === reqAsset.id); + if (asset.title) { + legacyResponse.title = asset.title.text; + } else if (asset.img) { + legacyResponse[requestAsset.img.type === NATIVE_IMAGE_TYPES.MAIN ? 'image' : 'icon'] = asset.img.url; + } else if (asset.data) { + legacyResponse[PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE[NATIVE_ASSET_TYPES_INVERSE[requestAsset.data.type]]] = asset.data.value; + } + } + return legacyResponse; +} + +/** + * Inverts key-values of an object. + */ +function inverse(obj) { + var retobj = {}; + for (var key in obj) { + retobj[obj[key]] = key; + } + return retobj; +} diff --git a/src/prebid.js b/src/prebid.js index ab336af08b6..c9e26225076 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -12,7 +12,7 @@ import { userSync } from './userSync.js'; import { config } from './config.js'; import { auctionManager } from './auctionManager.js'; import { filters, targeting } from './targeting.js'; -import { hook } from './hook.js'; +import {hook, wrapHook} from './hook.js'; import { loadSession } from './debugging.js'; import {includes} from './polyfill.js'; import { adunitCounter } from './adUnits.js'; @@ -129,6 +129,16 @@ function validateVideoMediaType(adUnit) { function validateNativeMediaType(adUnit) { const validatedAdUnit = deepClone(adUnit); const native = validatedAdUnit.mediaTypes.native; + // if native assets are specified in OpenRTB format, remove legacy assets and print a warn. + if (native.ortb) { + const legacyNativeKeys = Object.keys(CONSTANTS.NATIVE_KEYS).filter(key => CONSTANTS.NATIVE_KEYS[key].includes('hb_native_')); + const nativeKeys = Object.keys(native); + const intersection = nativeKeys.filter(nativeKey => legacyNativeKeys.includes(nativeKey)); + if (intersection.length > 0) { + logError(`when using native OpenRTB format, you cannot use legacy native properties. Deleting ${intersection} keys from request.`); + intersection.forEach(legacyKey => delete validatedAdUnit.mediaTypes.native[legacyKey]); + } + } if (native.image && native.image.sizes && !Array.isArray(native.image.sizes)) { logError('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.'); delete validatedAdUnit.mediaTypes.native.image.sizes; @@ -573,18 +583,28 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { * @param {String} requestOptions.auctionId * @alias module:pbjs.requestBids */ -$$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId, ortb2 } = {}) { - events.emit(REQUEST_BIDS); - const cbTimeout = timeout || config.getConfig('bidderTimeout'); - adUnits = adUnits || $$PREBID_GLOBAL$$.adUnits; - adUnits = (isArray(adUnits) ? adUnits : [adUnits]); - logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); - const ortb2Fragments = { - global: mergeDeep({}, config.getAnyConfig('ortb2') || {}, ortb2 || {}), - bidder: Object.fromEntries(Object.entries(config.getBidderConfig()).map(([bidder, cfg]) => [bidder, cfg.ortb2]).filter(([_, ortb2]) => ortb2 != null)) - } - return startAuction({bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ortb2Fragments}); -}, 'requestBids'); +$$PREBID_GLOBAL$$.requestBids = (function() { + const delegate = hook('async', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId, ortb2 } = {}) { + events.emit(REQUEST_BIDS); + const cbTimeout = timeout || config.getConfig('bidderTimeout'); + logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); + const ortb2Fragments = { + global: mergeDeep({}, config.getAnyConfig('ortb2') || {}, ortb2 || {}), + bidder: Object.fromEntries(Object.entries(config.getBidderConfig()).map(([bidder, cfg]) => [bidder, cfg.ortb2]).filter(([_, ortb2]) => ortb2 != null)) + } + return startAuction({bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ortb2Fragments}); + }, 'requestBids'); + + return wrapHook(delegate, function requestBids(req = {}) { + // if the request does not specify adUnits, clone the global adUnit array - before + // any hook has a chance to run. + // otherwise, if the caller goes on to use addAdUnits/removeAdUnits, any asynchronous logic + // in any hook might see their effects. + let adUnits = req.adUnits || $$PREBID_GLOBAL$$.adUnits; + req.adUnits = (isArray(adUnits) ? adUnits.slice() : [adUnits]); + return delegate.call(this, req); + }); +})(); export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ortb2Fragments } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 7eacb3c3fc0..d0c06f6ab1a 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -15,6 +15,7 @@ import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; const BID_WON = constants.EVENTS.BID_WON; const STALE_RENDER = constants.EVENTS.STALE_RENDER; +const WON_AD_IDS = new WeakSet(); const HANDLER_MAP = { 'Prebid Request': handleRenderRequest, @@ -113,6 +114,13 @@ function handleNativeRequest(reply, data, adObject) { logError(`Cannot find ad '${data.adId}' for x-origin event request`); return; } + + if (!WON_AD_IDS.has(adObject)) { + WON_AD_IDS.add(adObject); + auctionManager.addWinningBid(adObject); + events.emit(BID_WON, adObject); + } + switch (data.action) { case 'assetRequest': reply(getAssetMessage(data, adObject)); @@ -126,12 +134,7 @@ function handleNativeRequest(reply, data, adObject) { resizeRemoteCreative(adObject); break; default: - const trackerType = fireNativeTrackers(data, adObject); - if (trackerType === 'click') { - return; - } - auctionManager.addWinningBid(adObject); - events.emit(BID_WON, adObject); + fireNativeTrackers(data, adObject); } } @@ -190,7 +193,7 @@ function resizeRemoteCreative({ adId, adUnitCode, width, height }) { let element = getElementByAdUnit(elmType + ':not([style*="display: none"])'); if (element) { let elementStyle = element.style; - elementStyle.width = width + 'px'; + elementStyle.width = width ? width + 'px' : '100%'; elementStyle.height = height + 'px'; } else { logWarn(`Unable to locate matching page element for adUnitCode ${adUnitCode}. Can't resize it to ad's dimensions. Please review setup.`); diff --git a/src/storageManager.js b/src/storageManager.js index e703e0774d3..4ab224f8d9b 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,6 +1,5 @@ import {hook} from './hook.js'; import {hasDeviceAccess, checkCookieSupport, logError, logInfo, isPlainObject} from './utils.js'; -import {includes} from './polyfill.js'; import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; const moduleTypeWhiteList = ['core', 'prebid-module']; @@ -33,13 +32,11 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = const storageAllowed = bidderSettings.get(bidderCode, 'storageAllowed'); return storageAllowed == null ? false : storageAllowed; } + + const isVendorless = moduleTypeWhiteList.includes(moduleType); + function isValid(cb) { - if (includes(moduleTypeWhiteList, moduleType)) { - let result = { - valid: true - } - return cb(result); - } else if (!isBidderAllowed()) { + if (!isBidderAllowed()) { logInfo(`bidderSettings denied access to device storage for bidder '${bidderCode}'`); const result = {valid: false}; return cb(result); @@ -48,7 +45,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = let hookDetails = { hasEnforcementHook: false } - validateStorageEnforcement(gvlid, bidderCode || moduleName, hookDetails, function(result) { + validateStorageEnforcement(isVendorless, gvlid, bidderCode || moduleName, hookDetails, function(result) { if (result && result.hasEnforcementHook) { value = cb(result); } else { @@ -149,11 +146,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = const cookiesAreEnabled = function (done) { let cb = function (result) { if (result && result.valid) { - if (checkCookieSupport()) { - return true; - } - window.document.cookie = 'prebid.cookieTest'; - return window.document.cookie.indexOf('prebid.cookieTest') !== -1; + return checkCookieSupport(); } return false; } @@ -303,7 +296,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = /** * This hook validates the storage enforcement if gdprEnforcement module is included */ -export const validateStorageEnforcement = hook('async', function(gvlid, moduleName, hookDetails, callback) { +export const validateStorageEnforcement = hook('async', function(isVendorless, gvlid, moduleName, hookDetails, callback) { callback(hookDetails); }, 'validateStorageEnforcement'); diff --git a/src/userSync.js b/src/userSync.js index 96c3d662cad..674114e11f6 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -315,12 +315,16 @@ export function newUserSync(userSyncDependencies) { return publicApi; } -const browserSupportsCookies = !isSafariBrowser() && storage.cookiesAreEnabled(); - -export const userSync = newUserSync({ +export const userSync = newUserSync(Object.defineProperties({ config: config.getConfig('userSync'), - browserSupportsCookies: browserSupportsCookies -}); +}, { + browserSupportsCookies: { + get: function() { + // call storage lazily to give time for consent data to be available + return !isSafariBrowser() && storage.cookiesAreEnabled(); + } + } +})); /** * @typedef {Object} UserSyncDependencies diff --git a/test/helpers/consentData.js b/test/helpers/consentData.js index b59388d67f2..c708e397bd6 100644 --- a/test/helpers/consentData.js +++ b/test/helpers/consentData.js @@ -2,6 +2,11 @@ import {gdprDataHandler} from 'src/adapterManager.js'; import {GreedyPromise} from '../../src/utils/promise.js'; export function mockGdprConsent(sandbox, getConsentData = () => null) { + sandbox.stub(gdprDataHandler, 'enabled').get(() => true) sandbox.stub(gdprDataHandler, 'promise').get(() => GreedyPromise.resolve(getConsentData())); sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(getConsentData) } + +beforeEach(() => { + gdprDataHandler.reset(); +}) diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 4c5ff808bc0..3657f7da912 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -30,6 +30,21 @@ describe('33acrossBidAdapter:', function () { site: { id: siteId }, + device: { + ext: { + ttx: { + w: 1024, + h: 728, + pxr: 2, + vp: { + w: 800, + h: 600 + }, + ah: 500, + mtp: 0 + } + } + }, id: 'r1', regs: { ext: { @@ -117,7 +132,7 @@ describe('33acrossBidAdapter:', function () { this.withProduct = (prod = 'siab') => { ttxRequest.imp.forEach((imp) => { - Object.assign(imp, { + utils.mergeDeep(imp, { ext: { ttx: { prod @@ -129,6 +144,18 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withGpid = (gpid) => { + ttxRequest.imp.forEach((imp) => { + utils.mergeDeep(imp, { + ext: { + gpid + } + }); + }); + + return this; + }; + this.withGdprConsent = (consent, gdpr) => { Object.assign(ttxRequest, { user: { @@ -166,6 +193,12 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withDevice = (device) => { + utils.mergeDeep(ttxRequest, { device }); + + return this; + }; + this.withPageUrl = pageUrl => { Object.assign(ttxRequest.site, { page: pageUrl @@ -360,8 +393,21 @@ describe('33acrossBidAdapter:', function () { }; win = { parent: null, + devicePixelRatio: 2, + screen: { + width: 1024, + height: 728, + availHeight: 500 + }, + navigator: { + maxTouchPoints: 0 + }, document: { - visibilityState: 'visible' + visibilityState: 'visible', + documentElement: { + clientWidth: 800, + clientHeight: 600 + } }, innerWidth: 800, @@ -373,7 +419,6 @@ describe('33acrossBidAdapter:', function () { .withBanner() .build() ); - sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); sandbox.stub(document, 'getElementById').returns(element); @@ -755,6 +800,151 @@ describe('33acrossBidAdapter:', function () { const [ buildRequest ] = spec.buildRequests(bidRequests); validateBuiltServerRequest(buildRequest, serverRequest); }); + + context('when all the wrapping windows are accessible', function() { + it('returns the viewport dimensions of the top most accessible window', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withDevice({ + ext: { + ttx: { + vp: { + w: 6789, + h: 2345 + } + } + } + }) + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + + sandbox.stub(win, 'parent').value({ + document: { + documentElement: { + clientWidth: 1234, + clientHeight: 4567 + } + }, + parent: { + document: { + documentElement: { + clientWidth: 6789, + clientHeight: 2345 + } + }, + } + }); + + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); + }); + }); + + context('when one of the wrapping windows cannot be accessed', function() { + it('returns the viewport dimensions of the top most accessible window', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withDevice({ + ext: { + ttx: { + vp: { + w: 9876, + h: 5432 + } + } + } + }) + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const notAccessibleParentWindow = {}; + + Object.defineProperty(notAccessibleParentWindow, 'document', { + get() { throw new Error('fakeError'); } + }); + + sandbox.stub(win, 'parent').value({ + document: { + documentElement: { + clientWidth: 1234, + clientHeight: 4567 + } + }, + parent: { + parent: notAccessibleParentWindow, + document: { + documentElement: { + clientWidth: 9876, + clientHeight: 5432 + } + }, + } + }); + + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); + }); + }); + }); + + it('returns the screen dimensions', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withDevice({ + ext: { + ttx: { + w: 1024, + h: 728 + } + } + }) + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + + win.screen.width = 1024; + win.screen.height = 728; + + const [ buildRequest ] = spec.buildRequests(bidRequests); + + validateBuiltServerRequest(buildRequest, serverRequest); + }); + + context('when the window height is greater than the width', function() { + it('returns the smaller screen dimension as the width', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withDevice({ + ext: { + ttx: { + w: 728, + h: 1024 + } + } + }) + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + + win.screen.width = 1024; + win.screen.height = 728; + + win.innerHeight = 728; + win.innerWidth = 727; + + const [ buildRequest ] = spec.buildRequests(bidRequests); + + validateBuiltServerRequest(buildRequest, serverRequest); + }); }); context('when tab is inactive', function() { @@ -977,6 +1167,41 @@ describe('33acrossBidAdapter:', function () { }); }); + context('when Global Placement ID (gpid) is defined', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = {}; + }); + + it('passes the Global Placement ID (gpid) in the request', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withGpid('fakeGPID0') + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + + let copyBidRequest = utils.deepClone(bidRequests); + const bidRequestsWithGpid = copyBidRequest.map(function(bidRequest, index) { + return { + ...bidRequest, + ortb2Imp: { + ext: { + gpid: 'fakeGPID' + index + } + } + }; + }); + + const [ builtServerRequest ] = spec.buildRequests(bidRequestsWithGpid, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + context('when referer value is not available', function() { it('returns corresponding server requests without site.page set', function() { const bidderRequest = { diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 307830dd4ef..403ba63d7d1 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -11,8 +11,11 @@ describe('adnuntiusBidAdapter', function () { const GVLID = 855; const usi = utils.generateUUID() const meta = [{ key: 'usi', value: usi }] - const storage = getStorageManager({ gvlid: GVLID, moduleName: 'adnuntius' }) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + + before(() => { + const storage = getStorageManager({gvlid: GVLID, moduleName: 'adnuntius'}) + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + }); beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index da050b3af90..f32a0bf4ebe 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -118,7 +118,6 @@ describe('ColossussspAdapter', function () { ...bid, params: { placement_id: 0, - traffic: 'video', }, mediaTypes: { video: { diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c2f19fc3f78..58f8e1ad871 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -10,9 +10,10 @@ import { import { createBid } from 'src/bidfactory.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; +import * as refererDetection from 'src/refererDetection.js'; import { config } from '../../../src/config.js'; -import { NATIVE, VIDEO } from '../../../src/mediaTypes.js'; import * as storageManager from 'src/storageManager.js'; +import { NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { let utilsMock, sandbox; @@ -37,6 +38,183 @@ describe('The Criteo bidding adapter', function () { sandbox.restore(); }); + describe('getUserSyncs', function () { + const syncOptionsIframeEnabled = { + iframeEnabled: true + }; + + const expectedHash = { + cw: true, + lsw: true, + origin: 'criteoPrebidAdapter', + requestId: '123456', + tld: 'www.abc.com', + topUrl: 'www.abc.com', + version: '$prebid.version$'.replace(/\./g, '_'), + }; + + let randomStub, + getConfigStub, + getRefererInfoStub, + cookiesAreEnabledStub, + localStorageIsEnabledStub, + getCookieStub, + getDataFromLocalStorageStub; + + beforeEach(function () { + getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('criteo.fastBidVersion').returns('none'); + + randomStub = sinon.stub(Math, 'random'); + randomStub.returns(123456); + + getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + domain: 'www.abc.com' + }); + + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + localStorageIsEnabledStub.returns(true); + + getCookieStub = sinon.stub(storage, 'getCookie') + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + randomStub.restore(); + getConfigStub.restore(); + getRefererInfoStub.restore(); + cookiesAreEnabledStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub.restore(); + getDataFromLocalStorageStub.restore(); + }); + + it('should not trigger sync if publisher is using fast bid', function () { + getConfigStub.withArgs('criteo.fastBidVersion').returns('latest'); + + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); + + expect(userSyncs).to.eql([]); + }); + + it('should not trigger sync if publisher did not enable iframe based syncs', function () { + const userSyncs = spec.getUserSyncs({ + iframeEnabled: false + }, undefined, undefined, undefined); + + expect(userSyncs).to.eql([]); + }); + + it('should not trigger sync if purpose one is not granted', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'ABC', + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + }; + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, gdprConsent, undefined); + + expect(userSyncs).to.eql([]); + }); + + it('forwards ids from cookies', function () { + const cookieData = { + 'cto_bundle': 'a', + 'cto_sid': 'b', + 'cto_lwid': 'c', + 'cto_idcpy': 'd', + 'cto_optout': 'e' + }; + + const expectedHashWithCookieData = { + ...expectedHash, + ...{ + bundle: cookieData['cto_bundle'], + localWebId: cookieData['cto_lwid'], + secureIdCookie: cookieData['cto_sid'], + uid: cookieData['cto_idcpy'], + optoutCookie: cookieData['cto_optout'] + } + }; + + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); + + expect(userSyncs).to.eql([{ + type: 'iframe', + url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com#${JSON.stringify(expectedHashWithCookieData, Object.keys(expectedHashWithCookieData).sort()).replace(/"/g, '%22')}` + }]); + }); + + it('forwards ids from local storage', function () { + const localStorageData = { + 'cto_bundle': 'a', + 'cto_sid': 'b', + 'cto_lwid': 'c', + 'cto_idcpy': 'd', + 'cto_optout': 'e' + }; + + const expectedHashWithLocalStorageData = { + ...expectedHash, + ...{ + bundle: localStorageData['cto_bundle'], + localWebId: localStorageData['cto_lwid'], + secureIdCookie: localStorageData['cto_sid'], + uid: localStorageData['cto_idcpy'], + optoutCookie: localStorageData['cto_optout'] + } + }; + + getDataFromLocalStorageStub.callsFake(localStorageName => localStorageData[localStorageName]); + + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); + + expect(userSyncs).to.eql([{ + type: 'iframe', + url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com#${JSON.stringify(expectedHashWithLocalStorageData, Object.keys(expectedHashWithLocalStorageData).sort()).replace(/"/g, '%22')}` + }]); + }); + + it('forwards gdpr data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'ABC', + vendorData: { + purpose: { + consents: { + 1: true + } + } + } + }; + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, gdprConsent, undefined); + + expect(userSyncs).to.eql([{ + type: 'iframe', + url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com&gdpr=1&gdpr_consent=ABC#${JSON.stringify(expectedHash).replace(/"/g, '%22')}` + }]); + }); + + it('forwards usp data', function () { + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, 'ABC'); + + expect(userSyncs).to.eql([{ + type: 'iframe', + url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com&us_privacy=ABC#${JSON.stringify(expectedHash).replace(/"/g, '%22')}` + }]); + }); + }); + describe('isBidRequestValid', function () { it('should return false when given an invalid bid', function () { const bid = { diff --git a/test/spec/modules/dataController_spec.js b/test/spec/modules/dataController_spec.js new file mode 100644 index 00000000000..25f55047377 --- /dev/null +++ b/test/spec/modules/dataController_spec.js @@ -0,0 +1,210 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {filterBidData, init} from 'modules/dataControllerModule/index.js'; +import {startAuction} from 'src/prebid.js'; + +describe('data controller', function () { + let spyFn; + + beforeEach(function () { + spyFn = sinon.spy(); + }); + + afterEach(function () { + config.resetConfig(); + }); + + describe('data controller', function () { + let result; + let callbackFn; + let req; + + beforeEach(function () { + init(); + result = null; + req = { + 'adUnits': [{ + 'bids': [ + { + 'bidder': 'ix', + 'userId': { + 'id5id': { + 'uid': 'ID5*19RudTU8mWiRdKfG-0E9oyyJCdbgb8MvcaEtPAxM29QZYT5DW9r01vBozMD93UZy', + 'ext': { + 'linkType': 2 + } + } + }, + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*UJzjz7J0FNIWPCp8fAmwGavBhGxnJ06V9umghosEVm4ZPjpn2iWahAoiPal59yKa', + 'atype': 1, + 'ext': { + 'linkType': 2 + } + } + ] + } + ], + + } + ] + }], + 'ortb2Fragments': { + 'bidder': { + 'ix': { + 'user': { + 'data': [ + { + 'name': 'permutive.com', + 'ext': { + 'segtax': 4 + }, + 'segment': [ + { + 'id': '777777' + }, + { + 'id': '888888' + } + ] + } + ] + } + } + } + } + }; + callbackFn = function (request) { + result = request; + }; + }); + + afterEach(function () { + config.resetConfig(); + startAuction.getHooks({hook: filterBidData}).remove(); + }); + + it('filterEIDwhenSDA for All SDA ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterEIDwhenSDA: ['*'] + } + }; + config.setConfig(dataControllerConfiguration); + filterBidData(callbackFn, req); + expect(req.adUnits[0].bids[0].userIdAsEids).that.is.empty; + expect(req.adUnits[0].bids[0].userId).that.is.empty; + expect(req.ortb2Fragments.bidder.ix.user.ext.eids).that.is.empty; + expect(req.ortb2Fragments.global.user.ext.eids).that.is.empty; + }); + + it('filterEIDwhenSDA for available SAD permutive.com:4:777777 ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterEIDwhenSDA: ['permutive.com:4:777777'] + } + + }; + config.setConfig(dataControllerConfiguration); + filterBidData(callbackFn, req); + expect(req.adUnits[0].bids[0].userIdAsEids).that.is.empty; + expect(req.adUnits[0].bids[0].userId).that.is.empty; + + expect(req.ortb2Fragments.bidder.ix.user.ext.eids).that.is.empty; + expect(req.ortb2Fragments.global.user.ext.eids).that.is.empty; + }); + + it('filterEIDwhenSDA for unavailable SAD test.com:4:9999 ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterEIDwhenSDA: ['test.com:4:99999'] + } + }; + config.setConfig(dataControllerConfiguration); + filterBidData(callbackFn, req); + expect(req.adUnits[0].bids[0].userIdAsEids).that.is.not.empty; + expect(req.adUnits[0].bids[0].userId).that.is.not.empty; + }); + // Test for global + it('filterEIDwhenSDA for available global SAD test.com:4:777777 ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterEIDwhenSDA: ['test.com:5:11111'] + } + + }; + config.setConfig(dataControllerConfiguration); + let globalObject = { + 'ortb2Fragments': { + 'global': { + 'user': { + 'yob': 1985, + 'gender': 'm', + 'keywords': 'a,b', + 'data': [ + { + 'name': 'test.com', + 'ext': { + 'segtax': 5 + }, + 'segment': [ + { + 'id': '11111' + }, + { + 'id': '22222' + } + ] + } + ] + } + } + } + }; + let globalRequest = Object.assign({}, req, globalObject); + filterBidData(callbackFn, globalRequest); + expect(globalRequest.adUnits[0].bids[0].userIdAsEids).that.is.empty; + expect(globalRequest.adUnits[0].bids[0].userId).that.is.empty; + }); + + it('filterSDAwhenEID for id5-sync.com EID ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterSDAwhenEID: ['id5-sync.com'] + } + }; + config.setConfig(dataControllerConfiguration); + filterBidData(callbackFn, req); + expect(req.ortb2Fragments.bidder.ix.user.data).that.is.empty; + }); + + it('filterSDAwhenEID for All EID ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterSDAwhenEID: ['*'] + } + }; + config.setConfig(dataControllerConfiguration); + + filterBidData(callbackFn, req); + expect(req.ortb2Fragments.bidder.ix.user.data).that.is.empty; + expect(req.ortb2Fragments.global.user.data).that.is.empty; + }); + + it('filterSDAwhenEID for unavailable source test-sync.com EID ', function () { + let dataControllerConfiguration = { + 'dataController': { + filterSDAwhenEID: ['test-sync.com'] + } + }; + config.setConfig(dataControllerConfiguration); + filterBidData(callbackFn, req); + expect(req.ortb2Fragments.bidder.ix.user.data).that.is.not.empty; + }); + } + ); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index e0410ff7874..e71058824a4 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -302,6 +302,7 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('uid2', function() { const userId = { uid2: {'id': 'Sample_AD_Token'} @@ -316,6 +317,7 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('kpuid', function() { const userId = { kpuid: 'Sample_Token' @@ -330,6 +332,7 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('tncid', function() { const userId = { tncid: 'TEST_TNCID' @@ -344,6 +347,7 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('pubProvidedId', function() { const userId = { pubProvidedId: [{ @@ -437,6 +441,32 @@ describe('eids array generation for known sub-modules', function() { uids: [{ id: 'some-random-id-value', atype: 1 }] }); }); + + describe('ftrackId', () => { + it('should return the correct EID schema', () => { + expect(createEidsArray({ + ftrackId: { + DeviceID: ['aaa', 'bbb'], + SingleDeviceID: ['ccc', 'ddd'], + HHID: ['eee', 'fff'] + }, + foo: { + bar: 'baz' + }, + lorem: { + ipsum: '' + } + })).to.deep.equal([{ + atype: 1, + id: 'aaa|bbb', + ext: { + DeviceID: 'aaa|bbb', + SingleDeviceID: 'ccc|ddd', + HHID: 'eee|fff' + } + }]); + }); + }); }); describe('Negative case', function () { diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index d43ae8309bd..498bed29243 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -5,9 +5,6 @@ import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/f import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; import * as validationModule from 'modules/validationFpdModule/index.js'; -let enrichments = {...enrichmentModule}; -let validations = {...validationModule}; - describe('the first party data module', function () { afterEach(function () { config.resetConfig(); @@ -18,21 +15,37 @@ describe('the first party data module', function () { global: {key: 'value'}, bidder: {A: {bkey: 'bvalue'}} } - before(() => { + beforeEach(() => { reset(); + }); + + it('should run ortb2Fragments through fpd submodules', () => { registerSubmodules({ name: 'test', - queue: 2, processFpd: function () { return mockFpd; } }); - }) + const req = {ortb2Fragments: {}}; + return new Promise((resolve) => startAuctionHook(resolve, req)) + .then(() => { + expect(req.ortb2Fragments).to.eql(mockFpd); + }) + }); - it('should run ortb2Fragments through fpd submodules', () => { + it('should work with fpd submodules that return promises', () => { + registerSubmodules({ + name: 'test', + processFpd: function () { + return Promise.resolve(mockFpd); + } + }); const req = {ortb2Fragments: {}}; - startAuctionHook(() => null, req); - expect(req.ortb2Fragments).to.eql(mockFpd); + return new Promise((resolve) => { + startAuctionHook(resolve, req); + }).then(() => { + expect(req.ortb2Fragments).to.eql(mockFpd); + }); }); }); @@ -79,7 +92,6 @@ describe('the first party data module', function () { }); it('filters ortb2 data that is set', function () { - let validated; const global = { user: { data: {}, @@ -113,42 +125,42 @@ describe('the first party data module', function () { width = 1120; height = 750; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); - expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); - expect(validated.site.domain).to.equal('domain.com'); - expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); - expect(validated.user.data).to.be.undefined; - expect(validated.device).to.deep.to.equal({w: 1, h: 1}); - expect(validated.site.keywords).to.be.undefined; + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); + expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); + expect(validated.site.domain).to.equal('domain.com'); + expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); + expect(validated.user.data).to.be.undefined; + expect(validated.device).to.deep.to.equal({w: 1, h: 1}); + expect(validated.site.keywords).to.be.undefined; + }); }); it('should not overwrite existing data with default settings', function () { - let validated; const global = { site: { ref: 'https://referer.com' } }; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal('https://referer.com'); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal('https://referer.com'); + }); }); it('should allow overwrite default data with setConfig', function () { - let validated; const global = { site: { ref: 'https://referer.com' } }; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal('https://referer.com'); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal('https://referer.com'); + }); }); it('should filter all data', function () { - let validated; let global = { imp: [], site: { @@ -179,15 +191,13 @@ describe('the first party data module', function () { adServerCurrency: 'USD' } }; - config.setConfig({'firstPartyData': {skipEnrichments: true}}); - - ({global: validated} = processFpd({global})); - expect(validated).to.deep.equal({}); + return processFpd({global}).then(({global: validated}) => { + expect(validated).to.deep.equal({}); + }); }); it('should add enrichments but not alter any arbitrary ortb2 data', function () { - let validated; let global = { site: { ext: { @@ -205,12 +215,12 @@ describe('the first party data module', function () { }, cur: ['USD'] }; - - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal(getRefererInfo().referer); - expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); - expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); - expect(validated.cur).to.deep.equal(['USD']); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); + expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); + expect(validated.cur).to.deep.equal(['USD']); + }) }); it('should filter bidderConfig data', function () { @@ -230,12 +240,13 @@ describe('the first party data module', function () { } }; - const {bidder: validated} = processFpd({bidder}); - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.be.undefined; - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + return processFpd({bidder}).then(({bidder: validated}) => { + expect(validated.bidderA).to.not.be.undefined; + expect(validated.bidderA.user.data).to.be.undefined; + expect(validated.bidderA.user.keywords).to.equal('test'); + expect(validated.bidderA.site.keywords).to.equal('other'); + expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + }) }); it('should not filter bidderConfig data as it is valid', function () { @@ -255,17 +266,16 @@ describe('the first party data module', function () { } }; - const {bidder: validated} = processFpd({bidder}); - - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + return processFpd({bidder}).then(({bidder: validated}) => { + expect(validated.bidderA).to.not.be.undefined; + expect(validated.bidderA.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); + expect(validated.bidderA.user.keywords).to.equal('test'); + expect(validated.bidderA.site.keywords).to.equal('other'); + expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + }); }); it('should not set default values if skipEnrichments is turned on', function () { - let validated; config.setConfig({'firstPartyData': {skipEnrichments: true}}); let global = { @@ -281,15 +291,15 @@ describe('the first party data module', function () { } }; - ({global: validated} = processFpd({global})); - expect(validated.device).to.be.undefined; - expect(validated.site.ref).to.be.undefined; - expect(validated.site.page).to.be.undefined; - expect(validated.site.domain).to.be.undefined; + return processFpd({global}).then(({global: validated}) => { + expect(validated.device).to.be.undefined; + expect(validated.site.ref).to.be.undefined; + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + }); }); it('should not validate ortb2 data if skipValidations is turned on', function () { - let validated; config.setConfig({'firstPartyData': {skipValidations: true}}); let global = { @@ -304,8 +314,9 @@ describe('the first party data module', function () { } }; - ({global: validated} = processFpd({global})); - expect(validated.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); + return processFpd({global}).then(({global: validated}) => { + expect(validated.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); + }); }); }); }); diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index 333a8cce624..2c3f3f8576c 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -308,107 +308,133 @@ describe('FTRACK ID System', () => { }); }); - describe('Uses ftrack getUserIdsAsEids() method', () => { - it('getUserIdsAsEids using the ftrack submodule and gets three ids (HHID, DeviceId, SingleDeviceId)', () => { + describe('pbjs "get id" methods', () => { + // The full set of ftrack IDs to test against + let expectedIds = { + HHID: ['household_test_id'], + DeviceID: ['device_test_id'], + SingleDeviceID: ['single_device_test_id'] + }; + // The full config mock + let userSyncConfigMock = { + userSync: { + auctionDelay: 10, + userIds: [{ + name: 'ftrack', + value: { + ftrackId: expectedIds + } + }] + } + }; + // The full eids response + let expectedEids = [{ + id: 'device_test_id', + atype: 1, + ext: { + HHID: expectedIds.HHID.join('|'), + DeviceID: expectedIds.DeviceID.join('|'), + SingleDeviceID: expectedIds.SingleDeviceID.join('|') + } + }]; + + beforeEach(() => { init(config); setSubmoduleRegistry([ftrackIdSubmodule]); + }); - const ids = { - ftrackId: { - HHID: ['household_test_id'], - DeviceID: ['device_test_id'], - SingleDeviceID: ['single_device_test_id'] - } + afterEach(() => { + // Reset expectedIds to the default values because some tests overwrite them + expectedIds = { + HHID: ['household_test_id'], + DeviceID: ['device_test_id'], + SingleDeviceID: ['single_device_test_id'] }; - config.setConfig({ - userSync: { - auctionDelay: 10, - userIds: [{ - name: 'ftrack', value: ids, - }] - } - }); - - getGlobal().getUserIdsAsync().then((ids) => { - expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ - id: 'device_test_id', - atype: 1, - ext: { - HHID: 'household_test_id', - DeviceID: 'device_test_id', - SingleDeviceID: 'single_device_test_id', - } - }]); - }); }); - it('gets only the deviceId', () => { - init(config); - setSubmoduleRegistry([ftrackIdSubmodule]); + describe('pbjs.getUserIdsAsync()', () => { + it('should return the IDs in the correct schema', () => { + config.setConfig(userSyncConfigMock); - const ids = { ftrackId: { DeviceID: ['device_test_id'] } }; - config.setConfig({ - userSync: { - auctionDelay: 10, - userIds: [{ - name: 'ftrack', value: ids, - }] - } + getGlobal().getUserIdsAsync().then(ids => { + expect(ids).to.deep.equal({ + ftrackId: expectedIds + }); + }); }); + }); + + describe('pbjs.getUserIds()', () => { + it('should return the IDs in the correct schema', () => { + config.setConfig(userSyncConfigMock); - getGlobal().getUserIdsAsync().then((ids) => { - expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ - id: 'device_test_id', - atype: 1, - ext: { DeviceID: 'device_test_id' } - }]); + expect(getGlobal().getUserIds()).to.deep.equal({ + 'ftrackId': expectedIds + }); }); }); - it('gets only the user household id', () => { - init(config); - setSubmoduleRegistry([ftrackIdSubmodule]); + describe('pbjs.getUserIdsAsEids()', () => { + it('should return the correct EIDs schema', () => { + let userSyncConfig = Object.assign({}, userSyncConfigMock); - const ids = { ftrackId: { HHID: ['household_test_id'], } }; - config.setConfig({ - userSync: { - auctionDelay: 10, - userIds: [{ - name: 'ftrack', value: ids, - }] - } - }); + config.setConfig(userSyncConfig); - getGlobal().getUserIdsAsync().then((ids) => { - expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ - id: '', - atype: 1, - ext: { HHID: 'household_test_id' } - }]); + expect(getGlobal().getUserIdsAsEids()).to.deep.equal(expectedEids); }); - }); - it('gets only the deviceId', () => { - init(config); - setSubmoduleRegistry([ftrackIdSubmodule]); + describe('by ID type:', () => { + it('- DeviceID', () => { + let userSyncConfig = Object.assign({}, userSyncConfigMock); - const ids = { ftrackId: { SingleDeviceID: ['single_device_test_id'] } }; - config.setConfig({ - userSync: { - auctionDelay: 10, - userIds: [{ - name: 'ftrack', value: ids, - }] - } - }); + userSyncConfig.userSync.userIds[0].value.ftrackId = { + DeviceID: ['device_test_id'] + }; + + let expectedEidsClone = expectedEids.slice(); + expectedEidsClone[0].ext = { + DeviceID: expectedIds.DeviceID.join('|') + }; + + config.setConfig(userSyncConfig); + + expect(getGlobal().getUserIdsAsEids()).to.deep.equal(expectedEidsClone); + }); + + it('- HHID', () => { + let userSyncConfig = Object.assign({}, userSyncConfigMock); + userSyncConfig.userSync.userIds[0].value.ftrackId = { + HHID: ['household_test_id'] + }; - getGlobal().getUserIdsAsync().then((ids) => { - expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ - id: '', - atype: 1, - ext: { SingleDeviceID: 'single_device_test_id' } - }]); + let expectedEidsClone = expectedEids.slice(); + expectedEidsClone[0].id = ''; + expectedEidsClone[0].ext = { + HHID: expectedIds.HHID.join('|') + }; + + config.setConfig(userSyncConfig); + + expect(getGlobal().getUserIdsAsEids()).to.deep.equal(expectedEidsClone); + }); + + it('- SingleDeviceID', () => { + let userSyncConfig = Object.assign({}, userSyncConfigMock); + userSyncConfig.userSync.userIds[0].value.ftrackId = { + SingleDeviceID: ['single_device_test_id'] + }; + + let expectedEidsClone = expectedEids.slice(); + expectedEidsClone[0].id = ''; + expectedEidsClone[0].ext = { + SingleDeviceID: expectedIds.SingleDeviceID.join('|') + }; + + config.setConfig(userSyncConfig); + + expect(getGlobal().getUserIdsAsEids()).to.deep.equal(expectedEidsClone); + }); }); }); - }); + }) }); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index a78f5ac948e..ddd522decab 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -10,13 +10,16 @@ import { purpose2Rule, enableAnalyticsHook, getGvlid, - internal + internal, STRICT_STORAGE_ENFORCEMENT } from 'modules/gdprEnforcement.js'; import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { validateStorageEnforcement } from 'src/storageManager.js'; import * as events from 'src/events.js'; +import 'modules/appnexusBidAdapter.js'; // some tests expect this to be in the adapter registry +import 'src/prebid.js' +import {hook} from '../../../src/hook.js'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -97,6 +100,10 @@ describe('gdpr enforcement', function () { } }; + before(() => { + hook.ready(); + }); + after(function () { validateStorageEnforcement.getHooks({ hook: deviceAccessHook }).remove(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); @@ -143,13 +150,13 @@ describe('gdpr enforcement', function () { } }); - deviceAccessHook(nextFnSpy); + deviceAccessHook(nextFnSpy, false); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: false } - sinon.assert.calledWith(nextFnSpy, undefined, undefined, result); + sinon.assert.calledWith(nextFnSpy, false, undefined, undefined, result); }); it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { @@ -171,8 +178,8 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 5, 'rubicon'); + deviceAccessHook(nextFnSpy, false, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, false, 5, 'rubicon'); expect(logWarnSpy.callCount).to.equal(0); }); @@ -194,8 +201,8 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 3, 'rubicon'); + deviceAccessHook(nextFnSpy, false, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, false, 3, 'rubicon'); expect(logWarnSpy.callCount).to.equal(1); }); @@ -217,13 +224,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, false, 1, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, false, 1, 'appnexus', result); }); it('should use gvlMapping set by publisher', function() { @@ -248,13 +255,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, false, 1, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, false, 4, 'appnexus', result); config.resetConfig(); }); @@ -283,16 +290,43 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, false, 1, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, false, 4, 'appnexus', result); config.resetConfig(); curBidderStub.restore(); }); + + it(`should mark module as vendorless for rule validation when isVendorless = true and ${STRICT_STORAGE_ENFORCEMENT} is set`, () => { + setEnforcementConfig({ + [STRICT_STORAGE_ENFORCEMENT]: true + }); + let consentData = { + vendorData: staticConfig.consentData.getTCData, + gdprApplies: true + } + gdprDataHandlerStub.returns(consentData); + const validate = sinon.stub().callsFake(() => true); + deviceAccessHook(nextFnSpy, true, 123, 'mockModule', undefined, {validate}); + expect(validate.args[0][4]('mockModule')).to.be.true; + }); + + it(`should not enforce consent for vendorless modules if ${STRICT_STORAGE_ENFORCEMENT} is not set`, () => { + setEnforcementConfig({}); + let consentData = { + vendorData: staticConfig.consentData.getTCData, + gdprApplies: true + } + gdprDataHandlerStub.returns(consentData); + const validate = sinon.stub().callsFake(() => false); + deviceAccessHook(nextFnSpy, true, 123, 'mockModule', undefined, {validate}); + sinon.assert.callCount(validate, 0); + sinon.assert.calledWith(nextFnSpy, true, 123, 'mockModule', {hasEnforcementHook: true, valid: true}); + }) }); describe('userSyncHook', function () { @@ -888,6 +922,33 @@ describe('gdpr enforcement', function () { expect(isAllowed).to.equal(true); }); + describe('when module does not need vendor consent', () => { + Object.entries({ + 'storage': 1, + 'basicAds': 2, + 'measurement': 7 + }).forEach(([purpose, purposeNo]) => { + describe(`for purpose ${purpose}`, () => { + const rule = createGdprRule(purpose); + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, consentGiven]) => { + it(`should be ${t} when purpose is ${t}`, () => { + const consent = utils.deepClone(consentData); + consent.vendorData.purpose.consents[purposeNo] = consentGiven; + // vendor consent (and gvlid) should be ignored + consent.vendorData.vendor.consents[123] = !consentGiven; + // take legitimate interest out of the picture for this test + consent.vendorData.purpose.legitimateInterests = {}; + const actual = validateRules(rule, consent, 'mockModule', 123, () => true); + expect(actual).to.equal(consentGiven); + }) + }) + }) + }) + }) + describe('Purpose 2 special case', function () { const consentDataWithLIFalse = utils.deepClone(consentData); consentDataWithLIFalse.vendorData.purpose.legitimateInterests['2'] = false; diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index c4e9a86eec2..17fff31f132 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -279,8 +279,8 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(3); }); - it('should set the correct pi param if product param is found and is equal to skin', function () { - const request = { ...bidRequests[0], params: { ...zoneParam, product: 'Skin' } }; + it('should set the correct pi param if product param is found and is equal to skins', function () { + const request = { ...bidRequests[0], params: { ...zoneParam, product: 'Skins' } }; const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(8); }); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 086ec305ccc..243b702f03d 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -3462,6 +3462,7 @@ describe('IndexexchangeAdapter', function () { it('should not save error data into localstorage if consent is not given', () => { config.setConfig({ deviceAccess: false }); + storage.localStorageIsEnabled.restore(); // let core manage device access const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); bid.params.size = ['400', 100]; expect(spec.isBidRequestValid(bid)).to.be.false; diff --git a/test/spec/modules/microadBidAdapter_spec.js b/test/spec/modules/microadBidAdapter_spec.js index ff0b705594b..cca337c83f2 100644 --- a/test/spec/modules/microadBidAdapter_spec.js +++ b/test/spec/modules/microadBidAdapter_spec.js @@ -266,35 +266,70 @@ describe('microadBidAdapter', () => { }); }); - it('should add Liveramp identity link if it is available in request parameters', () => { - const bidRequestWithLiveramp = Object.assign({}, bidRequestTemplate, { - userId: {idl_env: 'idl-env-sample'} + it('should not add Liveramp identity link and Audience ID if it is not available in request parameters', () => { + const bidRequestWithOutLiveramp = Object.assign({}, bidRequestTemplate, { + userId: {} }); - const requests = spec.buildRequests([bidRequestWithLiveramp], bidderRequest) + const requests = spec.buildRequests([bidRequestWithOutLiveramp], bidderRequest) requests.forEach(request => { expect(request.data).to.deep.equal( Object.assign({}, expectedResultTemplate, { - cbt: request.data.cbt, - idl_env: 'idl-env-sample' + cbt: request.data.cbt }) ); }) }); - it('should not add Liveramp identity link if it is not available in request parameters', () => { - const bidRequestWithLiveramp = Object.assign({}, bidRequestTemplate, { - userId: {} - }); - const requests = spec.buildRequests([bidRequestWithLiveramp], bidderRequest) - const expectedResult = Object.assign({}, expectedResultTemplate) - requests.forEach(request => { - expect(request.data).to.deep.equal( - Object.assign({}, expectedResultTemplate, { - cbt: request.data.cbt + Object.entries({ + 'IM-UID': { + userId: {imuid: 'imuid-sample'}, + expected: {aids: JSON.stringify([{type: 6, id: 'imuid-sample'}])} + }, + 'ID5 ID': { + userId: {id5id: {uid: 'id5id-sample'}}, + expected: {aids: JSON.stringify([{type: 8, id: 'id5id-sample'}])} + }, + 'Unified ID': { + userId: {tdid: 'unified-sample'}, + expected: {aids: JSON.stringify([{type: 9, id: 'unified-sample'}])} + }, + 'Novatiq Snowflake ID': { + userId: {novatiq: {snowflake: 'novatiq-sample'}}, + expected: {aids: JSON.stringify([{type: 10, id: 'novatiq-sample'}])} + }, + 'Parrable ID': { + userId: {parrableId: {eid: 'parrable-sample'}}, + expected: {aids: JSON.stringify([{type: 11, id: 'parrable-sample'}])} + }, + 'AudienceOne User ID': { + userId: {dacId: {id: 'audience-one-sample'}}, + expected: {aids: JSON.stringify([{type: 12, id: 'audience-one-sample'}])} + }, + 'Ramp ID and Liveramp identity': { + userId: {idl_env: 'idl-env-sample'}, + expected: {idl_env: 'idl-env-sample', aids: JSON.stringify([{type: 13, id: 'idl-env-sample'}])} + }, + 'Criteo ID': { + userId: {criteoId: 'criteo-id-sample'}, + expected: {aids: JSON.stringify([{type: 14, id: 'criteo-id-sample'}])} + }, + 'Shared ID': { + userId: {pubcid: 'shared-id-sample'}, + expected: {aids: JSON.stringify([{type: 15, id: 'shared-id-sample'}])} + } + }).forEach(([test, arg]) => { + it(`should add ${test} if it is available in request parameters`, () => { + const bidRequestWithUserId = { ...bidRequestTemplate, userId: arg.userId } + const requests = spec.buildRequests([bidRequestWithUserId], bidderRequest) + requests.forEach((request) => { + expect(request.data).to.deep.equal({ + ...expectedResultTemplate, + cbt: request.data.cbt, + ...arg.expected }) - ); + }) }) - }); + }) }); describe('interpretResponse', () => { diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 0690c7f90e1..be6b07f9acc 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -1,5 +1,11 @@ import { expect } from 'chai' import { spec, BidDataMap } from 'modules/nativoBidAdapter.js' +import { + getSizeWildcardPrice, + getMediaWildcardPrices, + sizeToString, + parseFloorPriceData, +} from '../../../modules/nativoBidAdapter' describe('bidDataMap', function () { it('Should fail gracefully if no key value pairs have been added and no key is sent', function () { @@ -94,7 +100,7 @@ describe('nativoBidAdapterTests', function () { const bidRequestString = JSON.stringify(bidRequest) let bidRequests - beforeEach(function() { + beforeEach(function () { // Clone bidRequest each time bidRequests = [JSON.parse(bidRequestString)] }) @@ -119,6 +125,20 @@ describe('nativoBidAdapterTests', function () { expect(request.url).to.include('ntv_pas') }) + it('ntv_url parameter should NOT be empty even if the utl parameter was set as an empty value', function () { + bidRequests[0].params.url = '' + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + + expect(request.url).to.exist + expect(request.url).to.be.a('string') + expect(request.url).to.not.be.empty + }) + it('url should NOT contain placement specific query string parameters if placementId option is not provided', function () { bidRequests[0].params = {} const request = spec.buildRequests(bidRequests, { @@ -489,3 +509,130 @@ describe('Response to Request Filter Flow', () => { expect(request.url).to.include('ntv_ctf=234') }) }) + +describe('sizeToString', () => { + it('Formats size array correctly', () => { + const sizeString = sizeToString([300, 250]) + expect(sizeString).to.be.equal('300x250') + }) + + it('Returns an empty array for invalid data', () => { + // Not an array + let sizeString = sizeToString(300, 350) + expect(sizeString).to.be.equal('') + // Single entry + sizeString = sizeToString([300]) + expect(sizeString).to.be.equal('') + // Undefined + sizeString = sizeToString(undefined) + expect(sizeString).to.be.equal('') + }) +}) + +describe('getSizeWildcardPrice', () => { + it('Generates the correct floor price data', () => { + let floorPrice = { + currency: 'USD', + floor: 1.0, + } + let getFloorMock = () => { + return floorPrice + } + let floorMockSpy = sinon.spy(getFloorMock) + let bidRequest = { + getFloor: floorMockSpy, + mediaTypes: { + banner: { + sizes: [300, 250], + }, + }, + } + + let result = getSizeWildcardPrice(bidRequest, 'banner') + expect( + floorMockSpy.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*', + }) + ).to.be.true + expect(result).to.equal(floorPrice) + }) +}) + +describe('getMediaWildcardPrices', () => { + it('Generates the correct floor price data', () => { + let defaultFloorPrice = { + currency: 'USD', + floor: 1.1, + } + let sizefloorPrice = { + currency: 'USD', + floor: 2.2, + } + let getFloorMock = ({ currency, mediaType, size }) => { + if (Array.isArray(size)) return sizefloorPrice + + return defaultFloorPrice + } + let floorMockSpy = sinon.spy(getFloorMock) + let bidRequest = { + getFloor: floorMockSpy, + mediaTypes: { + banner: { + sizes: [300, 250], + }, + }, + } + + let result = getMediaWildcardPrices(bidRequest, ['*', [300, 250]]) + expect( + floorMockSpy.calledWith({ + currency: 'USD', + mediaType: '*', + size: '*', + }) + ).to.be.true + expect( + floorMockSpy.calledWith({ + currency: 'USD', + mediaType: '*', + size: [300, 250], + }) + ).to.be.true + expect(result).to.deep.equal({ '*': 1.1, '300x250': 2.2 }) + }) +}) + +describe('parseFloorPriceData', () => { + it('Generates the correct floor price data', () => { + let defaultFloorPrice = { + currency: 'USD', + floor: 1.1, + } + let sizefloorPrice = { + currency: 'USD', + floor: 2.2, + } + let getFloorMock = ({ currency, mediaType, size }) => { + if (Array.isArray(size)) return sizefloorPrice + + return defaultFloorPrice + } + let floorMockSpy = sinon.spy(getFloorMock) + let bidRequest = { + getFloor: floorMockSpy, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + } + + let result = parseFloorPriceData(bidRequest) + expect(result).to.deep.equal({ + '*': { '*': 1.1, '300x250': 2.2 }, + banner: { '*': 1.1, '300x250': 2.2 }, + }) + }) +}) diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 1e81abda57e..ff600d4ef93 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -87,6 +87,84 @@ const REQUEST = { ] }; +const NATIVE_ORTB_MTO = { + ortb: { + context: 3, + plcmttype: 2, + eventtrackers: [ + { + event: 1, + methods: [ + 1 + ] + }, + { + event: 2, + methods: [ + 2 + ] + } + ], + assets: [ + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 250 + } + }, + { + id: 2, + required: 1, + img: { + type: 1, + w: 127, + h: 83 + } + }, + { + id: 3, + required: 1, + data: { + type: 1, + len: 25 + } + }, + { + id: 4, + required: 1, + title: { + len: 140 + } + }, + { + id: 5, + required: 1, + data: { + type: 2, + len: 40 + } + }, + { + id: 6, + required: 1, + data: { + type: 12, + len: 15 + } + }, + ], + ext: { + custom_param: { + key: 'custom_value' + } + }, + ver: '1.2' + } +} + const VIDEO_REQUEST = { 'account_id': '1', 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', @@ -1196,7 +1274,7 @@ describe('S2S Adapter', function () { } else { delete getGlobal().convertCurrency; } - }) + }); it(`should pick the ${expectDesc}`, () => { adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); @@ -1209,37 +1287,38 @@ describe('S2S Adapter', function () { }); }); - it('adds device.w and device.h even if the config lacks a device object', function () { - const _config = { - s2sConfig: CONFIG, - app: { bundle: 'com.test.app' }, - }; - - config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.device).to.deep.equal({ - w: window.innerWidth, - h: window.innerHeight - }); - expect(requestBid.app).to.deep.equal({ - bundle: 'com.test.app', - publisher: { 'id': '1' } - }); - }); - if (FEATURES.NATIVE) { - it('adds native request for OpenRTB', function () { - const _config = { - s2sConfig: CONFIG - }; + describe('native requests', function () { + it('adds device.w and device.h even if the config lacks a device object', function () { + const _config = { + s2sConfig: CONFIG, + app: { bundle: 'com.test.app' }, + }; - config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); + config.setConfig(_config); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.device).to.deep.equal({ + w: window.innerWidth, + h: window.innerHeight + }); + expect(requestBid.app).to.deep.equal({ + bundle: 'com.test.app', + publisher: { 'id': '1' } + }); + }); - expect(requestBid.imp[0].native).to.deep.equal({ - request: JSON.stringify({ + it('adds native request for OpenRTB', function () { + const _config = { + s2sConfig: CONFIG + }; + + config.setConfig(_config); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[0].requestBody); + const ortbReq = JSON.parse(requestBid.imp[0].native.request); + expect(ortbReq).to.deep.equal({ + 'ver': '1.2', 'context': 1, 'plcmttype': 1, 'eventtrackers': [{ @@ -1283,23 +1362,43 @@ describe('S2S Adapter', function () { } } ] - }), - ver: '1.2' + }); + expect(requestBid.imp[0].native.ver).to.equal('1.2'); }); - }); - it('should not include ext.aspectratios if adunit\'s aspect_ratios do not define radio_width and ratio_height', () => { - const req = deepClone(REQUEST); - req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = {'min_width': 1, 'min_height': 2}; - prepRequest(req); - adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); - const nativeReq = JSON.parse(JSON.parse(server.requests[0].requestBody).imp[0].native.request); - const icons = nativeReq.assets.map((a) => a.img).filter((img) => img && img.type === 1); - expect(icons).to.have.length(1); - expect(icons[0].hmin).to.equal(2); - expect(icons[0].wmin).to.equal(1); - expect(deepAccess(icons[0], 'ext.aspectratios')).to.be.undefined; - }) + it('adds native ortb request for OpenRTB', function () { + const _config = { + s2sConfig: CONFIG + }; + + const openRtbNativeRequest = deepClone(REQUEST); + delete openRtbNativeRequest.ad_units[0].mediaTypes.native; + delete openRtbNativeRequest.ad_units[0].nativeParams; + + openRtbNativeRequest.ad_units[0].mediaTypes.native = NATIVE_ORTB_MTO; + prepRequest(openRtbNativeRequest); + + config.setConfig(_config); + adapter.callBids(openRtbNativeRequest, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[0].requestBody); + const nativeReq = JSON.parse(requestBid.imp[0].native.request); + expect(nativeReq).to.deep.equal(NATIVE_ORTB_MTO.ortb); + expect(requestBid.imp[0].native.ver).to.equal('1.2'); + }); + + it('should not include ext.aspectratios if adunit\'s aspect_ratios do not define radio_width and ratio_height', () => { + const req = deepClone(REQUEST); + req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = {'min_width': 1, 'min_height': 2}; + prepRequest(req); + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + const nativeReq = JSON.parse(JSON.parse(server.requests[0].requestBody).imp[0].native.request); + const icons = nativeReq.assets.map((a) => a.img).filter((img) => img && img.type === 1); + expect(icons).to.have.length(1); + expect(icons[0].hmin).to.equal(2); + expect(icons[0].wmin).to.equal(1); + expect(deepAccess(icons[0], 'ext.aspectratios')).to.be.undefined; + }); + }); } it('adds site if app is not present', function () { @@ -1875,6 +1974,163 @@ describe('S2S Adapter', function () { }); }); + it('should have extPrebid.schains present on req object if bidder specific schains were configured with pbjs', function () { + let bidRequest = utils.deepClone(BID_REQUESTS); + bidRequest[0].bids[0].schain = { + complete: 1, + nodes: [{ + asi: 'test.com', + hp: 1, + sid: '11111' + }], + ver: '1.0' + }; + + adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.ext.prebid.schains).to.deep.equal([ + { + bidders: ['appnexus'], + schain: { + complete: 1, + nodes: [ + { + asi: 'test.com', + hp: 1, + sid: '11111' + } + ], + ver: '1.0' + } + } + ]); + }); + + it('should skip over adding any bid specific schain entries that already exist on extPrebid.schains', function () { + let bidRequest = utils.deepClone(BID_REQUESTS); + bidRequest[0].bids[0].schain = { + complete: 1, + nodes: [{ + asi: 'pbjs.com', + hp: 1, + sid: '22222' + }], + ver: '1.0' + }; + + const s2sConfig = Object.assign({}, CONFIG, { + extPrebid: { + schains: [ + { + bidders: ['appnexus'], + schain: { + complete: 1, + nodes: [ + { + asi: 'pbs.com', + hp: 1, + sid: '11111' + } + ], + ver: '1.0' + } + } + ] + } + }); + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + + let requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.ext.prebid.schains).to.deep.equal([ + { + bidders: ['appnexus'], + schain: { + complete: 1, + nodes: [ + { + asi: 'pbs.com', + hp: 1, + sid: '11111' + } + ], + ver: '1.0' + } + } + ]); + }); + + it('should add a bidder name to pbs schain if the schain is equal to a pbjs one but the pbjs bidder name is not in the bidder array on the pbs side', function () { + let bidRequest = utils.deepClone(BID_REQUESTS); + bidRequest[0].bids[0].schain = { + complete: 1, + nodes: [{ + asi: 'test.com', + hp: 1, + sid: '11111' + }], + ver: '1.0' + }; + + bidRequest[0].bids[1] = { + bidder: 'rubicon', + params: { + accountId: 14062, + siteId: 70608, + zoneId: 498816 + } + }; + + const s2sConfig = Object.assign({}, CONFIG, { + bidders: ['rubicon', 'appnexus'], + extPrebid: { + schains: [ + { + bidders: ['rubicon'], + schain: { + complete: 1, + nodes: [ + { + asi: 'test.com', + hp: 1, + sid: '11111' + } + ], + ver: '1.0' + } + } + ] + } + }); + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + + let requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.ext.prebid.schains).to.deep.equal([ + { + bidders: ['rubicon', 'appnexus'], + schain: { + complete: 1, + nodes: [ + { + asi: 'test.com', + hp: 1, + sid: '11111' + } + ], + ver: '1.0' + } + } + ]); + }); + it('passes schain object in request', function () { const bidRequests = utils.deepClone(BID_REQUESTS); const schainObject = { diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index ead44f96bc9..9aec1f179a4 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -51,7 +51,8 @@ describe('RelaidoAdapter', function () { bidderRequest = { timeout: 1000, refererInfo: { - page: 'https://publisher.com/home' + page: 'https://publisher.com/home?aaa=test1&bbb=test2', + canonicalUrl: 'https://publisher.com/home' } }; serverResponse = { @@ -238,6 +239,7 @@ describe('RelaidoAdapter', function () { const request = data.bids[0]; expect(bidRequests.method).to.equal('POST'); expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.canonical_url_hash).to.equal('e6092f44a0044903ae3764126eedd6187c1d9f04'); expect(data.ref).to.equal(bidderRequest.refererInfo.page); expect(data.timeout_ms).to.equal(bidderRequest.timeout); expect(request.ad_unit_code).to.equal(bidRequest.adUnitCode); diff --git a/test/spec/modules/spotxBidAdapter_spec.js b/test/spec/modules/spotxBidAdapter_spec.js index 274ef60471b..74eaf669bce 100644 --- a/test/spec/modules/spotxBidAdapter_spec.js +++ b/test/spec/modules/spotxBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {config} from 'src/config.js'; +import {loadExternalScript} from '../../../src/adloader'; import {spec, GOOGLE_CONSENT} from 'modules/spotxBidAdapter.js'; describe('the spotx adapter', function () { @@ -18,14 +19,14 @@ describe('the spotx adapter', function () { }; describe('isBidRequestValid', function() { - var bid; + let bid; beforeEach(function() { bid = getValidBidObject(); }); it('should fail validation if the bid isn\'t defined or not an object', function() { - var result = spec.isBidRequestValid(); + let result = spec.isBidRequestValid(); expect(result).to.equal(false); @@ -92,7 +93,7 @@ describe('the spotx adapter', function () { }); describe('buildRequests', function() { - var bid, bidRequestObj; + let bid, bidRequestObj; beforeEach(function() { bid = getValidBidObject(); @@ -104,7 +105,7 @@ describe('the spotx adapter', function () { }); it('should build a very basic request', function() { - var request = spec.buildRequests([bid], bidRequestObj)[0]; + let request = spec.buildRequests([bid], bidRequestObj)[0]; expect(request.method).to.equal('POST'); expect(request.url).to.equal('https://search.spotxchange.com/openrtb/2.3/dados/12345?src_sys=prebid'); expect(request.bidRequest).to.equal(bidRequestObj); @@ -133,7 +134,7 @@ describe('the spotx adapter', function () { }); it('should change request parameters based on options sent', function() { - var request = spec.buildRequests([bid], bidRequestObj)[0]; + let request = spec.buildRequests([bid], bidRequestObj)[0]; expect(request.data.imp.video.ext).to.deep.equal({ sdk_name: 'Prebid 1+', versionOrtb: '2.3' @@ -239,7 +240,7 @@ describe('the spotx adapter', function () { }); it('should process premarket bids', function() { - var request; + let request; sinon.stub(Date, 'now').returns(1000); bid.params.pre_market_bids = [{ @@ -277,7 +278,7 @@ describe('the spotx adapter', function () { }); it('should pass GDPR params', function() { - var request; + let request; bidRequestObj.gdprConsent = { consentString: 'consent123', @@ -291,7 +292,7 @@ describe('the spotx adapter', function () { }); it('should pass CCPA us_privacy string', function() { - var request; + let request; bidRequestObj.uspConsent = '1YYY' @@ -300,7 +301,7 @@ describe('the spotx adapter', function () { }); it('should pass both GDPR params and CCPA us_privacy', function() { - var request; + let request; bidRequestObj.gdprConsent = { consentString: 'consent123', @@ -315,7 +316,7 @@ describe('the spotx adapter', function () { }); it('should pass min and max duration params', function() { - var request; + let request; bid.params.min_duration = 3 bid.params.max_duration = 15 @@ -327,7 +328,7 @@ describe('the spotx adapter', function () { }); it('should pass placement_type and position params', function() { - var request; + let request; bid.params.placement_type = 2 bid.params.position = 5 @@ -339,11 +340,11 @@ describe('the spotx adapter', function () { }); it('should pass page param and override refererInfo.referer', function() { - var request; + let request; bid.params.page = 'https://example.com'; - var origGetConfig = config.getConfig; + let origGetConfig = config.getConfig; sinon.stub(config, 'getConfig').callsFake(function (key) { if (key === 'pageUrl') { return 'https://www.spotx.tv'; @@ -358,7 +359,7 @@ describe('the spotx adapter', function () { }); it('should use refererInfo.referer if no page is passed', function() { - var request; + let request; request = spec.buildRequests([bid], bidRequestObj)[0]; @@ -366,9 +367,9 @@ describe('the spotx adapter', function () { }); it('should set ext.wrap_response to 0 when cache url is set and ignoreBidderCacheKey is true', function() { - var request; + let request; - var origGetConfig = config.getConfig; + let origGetConfig = config.getConfig; sinon.stub(config, 'getConfig').callsFake(function (key) { if (key === 'cache') { return { @@ -392,7 +393,7 @@ describe('the spotx adapter', function () { }); it('should pass price floor in USD from the floors module if available', function () { - var request; + let request; bid.getFloor = function () { return { currency: 'USD', floor: 3 }; @@ -406,7 +407,7 @@ describe('the spotx adapter', function () { }); it('should not pass price floor if price floors module gives a non-USD currency', function () { - var request; + let request; bid.getFloor = function () { return { currency: 'EUR', floor: 3 }; @@ -418,7 +419,7 @@ describe('the spotx adapter', function () { }); it('if floors module is not available, should pass price floor from price_floor param if available', function () { - var request; + let request; bid.params.price_floor = 2; @@ -429,7 +430,7 @@ describe('the spotx adapter', function () { }); describe('interpretResponse', function() { - var serverResponse, bidderRequestObj; + let serverResponse, bidderRequestObj; beforeEach(function() { bidderRequestObj = { @@ -503,7 +504,7 @@ describe('the spotx adapter', function () { }); it('should return an array of bid responses', function() { - var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + let responses = spec.interpretResponse(serverResponse, bidderRequestObj); expect(responses).to.be.an('array').with.length(2); expect(responses[0].cache_key).to.equal('cache123'); expect(responses[0].channel_id).to.equal(12345); @@ -548,9 +549,16 @@ describe('the spotx adapter', function () { }); describe('outstreamRender', function() { - var serverResponse, bidderRequestObj; + let serverResponse, bidderRequestObj; beforeEach(function() { + sinon.stub(window.document, 'getElementById').returns({ + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) {}) + }); + sinon.stub(window.document, 'createElement').returns({ + setAttribute: function () {} + }); bidderRequestObj = { bidRequest: { bids: [{ @@ -600,99 +608,76 @@ describe('the spotx adapter', function () { } }; }); + afterEach(function () { + window.document.getElementById.restore(); + window.document.createElement.restore(); + }); it('should attempt to insert the EASI script', function() { - var scriptTag; + window.document.getElementById.restore(); sinon.stub(window.document, 'getElementById').returns({ - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) + appendChild: sinon.stub().callsFake(function(script) {}), }); - var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + let responses = spec.interpretResponse(serverResponse, bidderRequestObj); + let attrs; responses[0].renderer.render(responses[0]); - - expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); - expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); - expect(scriptTag.getAttribute('data-spotx_channel_id')).to.equal('12345'); - expect(scriptTag.getAttribute('data-spotx_vast_url')).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache123'); - expect(scriptTag.getAttribute('data-spotx_ad_unit')).to.equal('incontent'); - expect(scriptTag.getAttribute('data-spotx_collapse')).to.equal('0'); - expect(scriptTag.getAttribute('data-spotx_autoplay')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_blocked_autoplay_override_mode')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_video_slot_can_autoplay')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_digitrust_opt_out')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('400'); - expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('300'); - expect(scriptTag.getAttribute('data-spotx_ad_mute')).to.equal('1'); - window.document.getElementById.restore(); + expect(loadExternalScript.called).to.be.true; + attrs = valuesToString(loadExternalScript.args[0][4]); + + expect(attrs['data-spotx_channel_id']).to.equal('12345'); + expect(attrs['data-spotx_vast_url']).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache123'); + expect(attrs['data-spotx_ad_unit']).to.equal('incontent'); + expect(attrs['data-spotx_collapse']).to.equal('0'); + expect(attrs['data-spotx_autoplay']).to.equal('1'); + expect(attrs['data-spotx_blocked_autoplay_override_mode']).to.equal('1'); + expect(attrs['data-spotx_video_slot_can_autoplay']).to.equal('1'); + expect(attrs['data-spotx_digitrust_opt_out']).to.equal('1'); + expect(attrs['data-spotx_content_width']).to.equal('400'); + expect(attrs['data-spotx_content_height']).to.equal('300'); + expect(attrs['data-spotx_ad_mute']).to.equal('1'); }); it('should append into an iframe', function() { - var scriptTag; + bidderRequestObj.bidRequest.bids[0].params.outstream_options.in_iframe = 'iframeId'; + window.document.getElementById.restore(); sinon.stub(window.document, 'getElementById').returns({ nodeName: 'IFRAME', - contentDocument: { - body: { - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) - } - } + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) {}), + contentDocument: {nodeName: 'IFRAME'} }); - bidderRequestObj.bidRequest.bids[0].params.outstream_options.in_iframe = 'iframeId'; - - var responses = spec.interpretResponse(serverResponse, bidderRequestObj); - + let responses = spec.interpretResponse(serverResponse, bidderRequestObj); responses[0].renderer.render(responses[0]); - - expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); - expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); - expect(scriptTag.getAttribute('data-spotx_channel_id')).to.equal('12345'); - expect(scriptTag.getAttribute('data-spotx_vast_url')).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache123'); - expect(scriptTag.getAttribute('data-spotx_ad_unit')).to.equal('incontent'); - expect(scriptTag.getAttribute('data-spotx_collapse')).to.equal('0'); - expect(scriptTag.getAttribute('data-spotx_autoplay')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_blocked_autoplay_override_mode')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_video_slot_can_autoplay')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_digitrust_opt_out')).to.equal('1'); - expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('400'); - expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('300'); - window.document.getElementById.restore(); + expect(loadExternalScript.called).to.be.true; + expect(loadExternalScript.args[0][3].nodeName).to.equal('IFRAME'); }); it('should adjust width and height to match slot clientWidth if playersize_auto_adapt is used', function() { - var scriptTag; - sinon.stub(window.document, 'getElementById').returns({ - clientWidth: 200, - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) - }); - var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + let responses = spec.interpretResponse(serverResponse, bidderRequestObj); responses[0].renderer.render(responses[0]); - - expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); - expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); - expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); - expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); - window.document.getElementById.restore(); + expect(loadExternalScript.args[0][4]['data-spotx_content_width']).to.equal('200'); + expect(loadExternalScript.args[0][4]['data-spotx_content_height']).to.equal('150'); }); it('should use a default 4/3 ratio if playersize_auto_adapt is used and response does not contain width or height', function() { delete serverResponse.body.seatbid[0].bid[0].w; delete serverResponse.body.seatbid[0].bid[0].h; - - var scriptTag; - sinon.stub(window.document, 'getElementById').returns({ - clientWidth: 200, - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) - }); - var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + let responses = spec.interpretResponse(serverResponse, bidderRequestObj); responses[0].renderer.render(responses[0]); - - expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); - expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); - expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); - expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); - window.document.getElementById.restore(); + expect(loadExternalScript.args[0][4]['data-spotx_content_width']).to.equal('200'); + expect(loadExternalScript.args[0][4]['data-spotx_content_height']).to.equal('150'); }); }); }); + +function valuesToString(obj) { + let newObj = {}; + for (let prop in obj) { + newObj[prop] = '' + obj[prop]; + } + return newObj; +} diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index f286eb3a1ff..d182b9db24c 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -678,8 +678,8 @@ describe('SSPBC adapter', function () { }); it('should send no syncs, if frame sync is not allowed', function () { - expect(syncResultImage).to.have.length(0); ; - expect(syncResultNone).to.have.length(0); ; + expect(syncResultImage).to.have.length(0); + expect(syncResultNone).to.have.length(0); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 625bb4ac6b7..5b43b78280b 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -59,7 +59,8 @@ describe('stroeerCore bid adapter', function () { timeout: 5000, auctionStart: 10000, refererInfo: { - referer: 'https://www.example.com/index.html' + page: 'https://www.example.com/monkey/index.html', + ref: 'https://www.example.com/?search=monkey' }, bids: [{ bidId: 'bid1', @@ -109,7 +110,7 @@ describe('stroeerCore bid adapter', function () { }); const createWindow = (href, params = {}) => { - let {parent, referrer, top, frameElement, placementElements = []} = params; + let {parent, top, frameElement, placementElements = []} = params; const protocol = (href.indexOf('https') === 0) ? 'https:' : 'http:'; const win = { @@ -126,7 +127,6 @@ describe('stroeerCore bid adapter', function () { } } }, - referrer, getElementById: id => find(placementElements, el => el.id === id) } }; @@ -169,7 +169,7 @@ describe('stroeerCore bid adapter', function () { } function setupNestedWindows(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { - const topWin = createWindow('http://www.abc.org/', {referrer: 'http://www.google.com/?query=monkey'}); + const topWin = createWindow('http://www.abc.org/'); topWin.innerHeight = 800; const midWin = createWindow('http://www.abc.org/', {parent: topWin, top: topWin, frameElement: createElement()}); @@ -310,10 +310,10 @@ describe('stroeerCore bid adapter', function () { const expectedJsonPayload = { 'id': AUCTION_ID, 'timeout': expectedTimeout, - 'ref': topWin.document.referrer, + 'ref': 'https://www.example.com/?search=monkey', 'mpa': true, 'ssl': false, - 'url': 'https://www.example.com/index.html', + 'url': 'https://www.example.com/monkey/index.html', 'bids': [{ 'sid': 'NDA=', 'bid': 'bid1', 'siz': [[300, 600], [160, 60]], 'viz': true }, { @@ -403,6 +403,30 @@ describe('stroeerCore bid adapter', function () { assert.lengthOf(serverRequestInfo.data.bids, 2); assert.notProperty(serverRequestInfo, 'uids'); }); + + it('should add schain if available', () => { + const schain = Object.freeze({ + ver: '1.0', + complete: 1, + 'nodes': [ + { + asi: 'exchange1.com', + sid: 'ABC', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + } + ] + }); + + const bidReq = buildBidderRequest(); + bidReq.bids.forEach(bid => bid.schain = schain); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.deepEqual(serverRequestInfo.data.schain, schain); + }); }); }); }); diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 4749a6a5919..739335e8f1f 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -311,6 +311,7 @@ describe('Taboola Adapter', function () { 'crid': '278195503434041083381', 'w': 300, 'h': 250, + 'exp': 60, 'lurl': 'http://us-trc.taboola.com/sample' } ], @@ -386,6 +387,31 @@ describe('Taboola Adapter', function () { const res = spec.interpretResponse(serverResponse, request) expect(res).to.deep.equal(expectedRes) }); + + it('should set the correct ttl form the response', function () { + // set exp-ttl to be 125 + const [bid] = serverResponse.body.seatbid[0].bid; + serverResponse.body.seatbid[0].bid[0].exp = 125; + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: bid.price, + creativeId: bid.crid, + ttl: 125, + netRevenue: false, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + meta: { + 'advertiserDomains': bid.adomain + }, + } + ]; + const res = spec.interpretResponse(serverResponse, request); + expect(res).to.deep.equal(expectedRes); + }); }) describe('userData', function () { diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js new file mode 100644 index 00000000000..3781768497b --- /dev/null +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -0,0 +1,239 @@ +import {getTopics, getTopicsData, processFpd} from '../../../modules/topicsFpdModule.js'; +import {deepClone} from '../../../src/utils.js'; + +describe('getTopicsData', () => { + function makeTopic(topic, modelv, taxv = '1') { + return { + topic, + taxonomyVersion: taxv, + modelVersion: modelv + } + } + + function byTaxClass(segments) { + return segments.reduce((memo, segment) => { + memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; + return memo; + }, {}) + } + + [ + { + t: 'no topics', + topics: [], + expected: [] + }, + { + t: 'single topic', + topics: [makeTopic(123, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics with the same model version', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'}, + {id: '321'} + ] + } + ] + }, + { + t: 'multiple topics with different model versions', + topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '1'}, + {id: '2'} + ] + }, + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '3'} + ] + } + ] + }, + { + t: 'multiple topics, some with a taxonomy version other than "1"', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics in multiple taxonomies', + taxonomies: { + '1': 600, + '2': 601 + }, + topics: [ + makeTopic(123, 'm1', '1'), + makeTopic(321, 'm1', '2'), + makeTopic(213, 'm2', '1'), + ], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + }, + { + ext: { + segtax: 601, + segclass: 'm1', + }, + segment: [ + {id: '321'} + ] + }, + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '213'} + ] + } + ] + } + ].forEach(({t, topics, expected, taxonomies}) => { + describe(`on ${t}`, () => { + it('should convert topics to user.data segments correctly', () => { + const actual = getTopicsData('mockName', topics, taxonomies); + expect(actual.length).to.eql(expected.length); + expected = byTaxClass(expected); + Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { + sinon.assert.match(datum, expected[key]); + expect(datum.name).to.equal('mockName'); + }) + }); + + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }) + }) + }) + }) +}); + +describe('getTopics', () => { + Object.entries({ + 'document with no browsingTopics': {}, + 'document that disallows topics': { + featurePolicy: { + allowsFeature: sinon.stub().returns(false) + } + }, + 'document that throws on featurePolicy': { + browsingTopics: sinon.stub(), + get featurePolicy() { + throw new Error() + } + }, + 'document that throws on browsingTopics': { + browsingTopics: sinon.stub().callsFake(() => { throw new Error(); }), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }, + }).forEach(([t, doc]) => { + it(`should resolve to an empty list on ${t}`, () => { + return getTopics(doc).then((topics) => { + expect(topics).to.eql([]); + }); + }) + }); + + it('should call `document.browsingTopics` when allowed', () => { + const topics = ['t1', 't2'] + return getTopics({ + browsingTopics: sinon.stub().returns(Promise.resolve(topics)), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }).then((actual) => { + expect(actual).to.eql(topics); + }) + }) +}) + +describe('processFpd', () => { + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}] + } + ]; + + it('should add topics data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + expect(global.user.data).to.eql(mockData); + }); + }); + + it('should apppend to existing user.data', () => { + const global = { + user: { + data: [ + {name: 'preexisting'}, + ] + } + }; + return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) + .then((data) => { + expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); + }); + }); + + it('should not modify fpd when there is no data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve([])}) + .then((data) => { + expect(data.global).to.eql({}); + }); + }); +}); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index b0269aaf077..bfcfce1ccb4 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -345,6 +345,209 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + }, + // outstream video only + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + adUnitCode: 'adunit-code-outstream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and incomplete outstream (missing size) + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6 + } + }, + mediaTypes: { + video: { + context: 'outstream' + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 3 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 4 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 5 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; undefined placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; invalid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 6 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, } ]; @@ -415,13 +618,16 @@ describe('triplelift adapter', function () { expect(payload.imp[0].tagid).to.equal('12345'); expect(payload.imp[0].floor).to.equal(1.0); expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // instream expect(payload.imp[1].tagid).to.equal('insteam_test'); expect(payload.imp[1].floor).to.equal(1.0); expect(payload.imp[1].video).to.exist.and.to.be.a('object'); + expect(payload.imp[1].video.placement).to.equal(1); // banner and outstream video - expect(payload.imp[2]).to.not.have.property('video'); + expect(payload.imp[2]).to.have.property('video'); expect(payload.imp[2]).to.have.property('banner'); expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[2].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); // banner and incomplete video expect(payload.imp[3]).to.not.have.property('video'); expect(payload.imp[3]).to.have.property('banner'); @@ -434,10 +640,51 @@ describe('triplelift adapter', function () { expect(payload.imp[5]).to.not.have.property('banner'); expect(payload.imp[5]).to.have.property('video'); expect(payload.imp[5].video).to.exist.and.to.be.a('object'); + expect(payload.imp[5].video.placement).to.equal(1); // banner and outream video and native - expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('video'); expect(payload.imp[6]).to.have.property('banner'); expect(payload.imp[6].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[6].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); + // outstream video only + expect(payload.imp[7]).to.have.property('video'); + expect(payload.imp[7]).to.not.have.property('banner'); + expect(payload.imp[7].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); + // banner and incomplete outstream (missing size); video request is permitted so banner can still monetize + expect(payload.imp[8]).to.have.property('video'); + expect(payload.imp[8]).to.have.property('banner'); + expect(payload.imp[8].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[8].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'context': 'outstream', 'placement': 3}); + }); + + it('should check for valid outstream placement values', function () { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + // outstream video; valid placement + expect(payload.imp[9]).to.not.have.property('banner'); + expect(payload.imp[9]).to.have.property('video'); + expect(payload.imp[9].video).to.exist.and.to.be.a('object'); + expect(payload.imp[9].video.placement).to.equal(3); + // outstream video; valid placement + expect(payload.imp[10]).to.not.have.property('banner'); + expect(payload.imp[10]).to.have.property('video'); + expect(payload.imp[10].video).to.exist.and.to.be.a('object'); + expect(payload.imp[10].video.placement).to.equal(4); + // outstream video; valid placement + expect(payload.imp[11]).to.not.have.property('banner'); + expect(payload.imp[11]).to.have.property('video'); + expect(payload.imp[11].video).to.exist.and.to.be.a('object'); + expect(payload.imp[11].video.placement).to.equal(5); + // outstream video; undefined placement + expect(payload.imp[12]).to.not.have.property('banner'); + expect(payload.imp[12]).to.have.property('video'); + expect(payload.imp[12].video).to.exist.and.to.be.a('object'); + expect(payload.imp[12].video.placement).to.equal(3); + // outstream video; invalid placement + expect(payload.imp[13]).to.not.have.property('banner'); + expect(payload.imp[13]).to.have.property('video'); + expect(payload.imp[13].video).to.exist.and.to.be.a('object'); + expect(payload.imp[13].video.placement).to.equal(3); }); it('should add tdid to the payload if included', function () { @@ -913,14 +1160,40 @@ describe('triplelift adapter', function () { iurl: 'https://s.adroll.com/a/IYR/N36/IYRN366MFVDITBAGNNT5U6.jpg', tl_source: 'tlx', advertiser_name: 'fake advertiser name', - adomain: ['basspro.com', 'internetalerts.org'] + adomain: ['basspro.com', 'internetalerts.org'], + media_type: 'banner' }, { imp_id: 1, crid: '10092_76480_i2j6qm8u', cpm: 9.99, - ad: 'The Trade Desk', - tlx_source: 'hdx' + ad: 'The Trade Desk', + tl_source: 'hdx', + media_type: 'video' + }, + // video bid on banner+outstream request + { + imp_id: 2, + crid: '5989_33264_352817187', + cpm: 20, + ad: '\n \n \t', + tl_source: 'hdx', + advertiser_name: 'zennioptical.com', + adomain: ['zennioptical.com'], + media_type: 'video' + }, + // banner bid on banner+outstream request + { + imp_id: 3, + crid: '5989_33264_352817187', + cpm: 20, + width: 970, + height: 250, + ad: 'ad-markup', + tl_source: 'hdx', + advertiser_name: 'zennioptical.com', + adomain: ['zennioptical.com'], + media_type: 'banner' } ] } @@ -946,13 +1219,13 @@ describe('triplelift adapter', function () { ] } }, - bidId: '30b31c1838de1e', + bidId: '30b31c1838de1e' }, { imp_id: 1, crid: '10092_76480_i2j6qm8u', cpm: 9.99, - ad: 'The Trade Desk', + ad: 'The Trade Desk', tlx_source: 'hdx', mediaTypes: { video: { @@ -960,7 +1233,89 @@ describe('triplelift adapter', function () { playerSize: [640, 480] } }, - bidId: '30b31c1838de1e', + bidId: '30b31c1838de1e' + }, + // banner and outstream + { + bidder: 'triplelift', + params: { + inventoryCode: 'testing_desktop_outstream', + floor: 1 + }, + nativeParams: {}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + }, + banner: { + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ] + }, + native: {} + }, + adUnitCode: 'video-outstream', + transactionId: '135061c3-f546-4e28-8a07-44c2fb58a958', + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ], + bidId: '73edc0ba8de203', + bidderRequestId: '3d81143328560b', + auctionId: 'f6427dc0-b954-4010-a76c-d498380796a2', + src: 'client', + bidRequestsCount: 2, + bidderRequestsCount: 2, + bidderWinsCount: 0 + }, + // banner and outstream + { + bidder: 'triplelift', + params: { + inventoryCode: 'testing_desktop_outstream', + floor: 1 + }, + nativeParams: {}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + }, + banner: { + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ] + }, + native: {} + }, + adUnitCode: 'video-outstream', + transactionId: '135061c3-f546-4e28-8a07-44c2fb58a958', + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ], + bidId: '73edc0ba8de203', + bidderRequestId: '3d81143328560b', + auctionId: 'f6427dc0-b954-4010-a76c-d498380796a2', + src: 'client', + bidRequestsCount: 2, + bidderRequestsCount: 2, + bidderWinsCount: 0 } ], refererInfo: { @@ -1007,16 +1362,29 @@ describe('triplelift adapter', function () { } ]; let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result).to.have.length(2); + expect(result).to.have.length(4); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); expect(Object.keys(result[1])).to.have.members(Object.keys(expectedResponse[1])); expect(result[0].ttl).to.equal(300); expect(result[1].ttl).to.equal(3600); }); + it('should identify format of bid and respond accordingly', function() { + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + expect(result[0].meta.mediaType).to.equal('native'); + expect(result[1].mediaType).to.equal('video'); + expect(result[1].meta.mediaType).to.equal('video'); + // video bid on banner+outstream request + expect(result[2].mediaType).to.equal('video'); + expect(result[2].meta.mediaType).to.equal('video'); + expect(result[2].vastXml).to.include('aid=148508128401385324170&inv_code=testing_mobile_outstream'); + // banner bid on banner+outstream request + expect(result[3].meta.mediaType).to.equal('banner'); + }) + it('should return multiple responses to support SRA', function () { let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result).to.have.length(2); + expect(result).to.have.length(4); }); it('should include the advertiser name in the meta field if available', function () { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 486018d41c4..77ba1396afb 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -698,11 +698,15 @@ describe('User ID', function () { config.resetConfig(); }); - it('fails initialization if opt out cookie exists', function () { + it('does not fetch ids if opt out cookie exists', function () { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); - config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - opt-out cookie found, exit module'); + const cfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + cfg.userSync.auctionDelay = 1; // to let init complete without an auction + config.setConfig(cfg); + return getGlobal().getUserIdsAsync().then((uid) => { + expect(uid).to.eql({}); + }) }); it('initializes if no opt out cookie exists', function () { diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 9e61768264f..dc178a9c485 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -81,6 +81,15 @@ const REQUEST = { } }; +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, { decodeSearchAsString: true }); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + describe('VidazooBidAdapter', function () { describe('validtae spec', function () { it('exists and is a function', function () { @@ -171,6 +180,7 @@ describe('VidazooBidAdapter', function () { prebidVersion: version, schain: BID.schain, res: `${window.top.screen.width}x${window.top.screen.height}`, + uqs: getTopWindowQueryParams(), 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', } diff --git a/test/spec/modules/videonowBidAdapter_spec.js b/test/spec/modules/videonowBidAdapter_spec.js new file mode 100644 index 00000000000..c9eb5ba0bbf --- /dev/null +++ b/test/spec/modules/videonowBidAdapter_spec.js @@ -0,0 +1,80 @@ +import {expect} from 'chai'; +import {spec} from 'modules/videonowBidAdapter'; + +describe('videonowBidAdapter', function () { + it('minimal params', function () { + expect(spec.isBidRequestValid({ + bidder: 'videonow', + params: { + pId: 'advDesktopBillboard' + }})).to.equal(true) + }) + + it('minimal params no placementId', function () { + expect(spec.isBidRequestValid({ + bidder: 'videonow', + params: { + currency: `GBP` + }})).to.equal(false) + }) + + it('generated_params common case', function () { + const bidRequestData = [{ + bidId: 'bid1234', + bidder: 'videonow', + params: { + pId: 'advDesktopBillboard', + currency: `GBP` + }, + sizes: [[240, 400]] + }]; + + const request = spec.buildRequests(bidRequestData); + const req_data = request[0].data; + + expect(req_data.places[0].id).to.equal(`bid1234`) + expect(req_data.places[0].placementId).to.equal(`advDesktopBillboard`) + expect(req_data.settings.currency).to.equal(`GBP`) + expect(req_data.places[0].sizes[0][0]).to.equal(240); + expect(req_data.places[0].sizes[0][1]).to.equal(400); + }); + + it('response_params common case', function () { + const bidRequestData = { + data: { + bidId: 'bid1234' + } + }; + + const serverResponse = { + body: { + bids: [ + { + 'displayCode': 'test html', + 'id': '123456', + 'cpm': 375, + 'currency': 'RUB', + 'placementId': 'profileName', + 'codeType': 'js', + 'size': { + 'width': 640, + 'height': 480 + } + } + ] + } + }; + + const bids = spec.interpretResponse(serverResponse, bidRequestData); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.requestId).to.equal('123456') + expect(bid.cpm).to.equal(375); + expect(bid.currency).to.equal('RUB'); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.ad).to.equal('test html'); + expect(bid.creativeId).to.equal(`123456`) + expect(bid.netRevenue).to.equal(true); + }); +}) diff --git a/test/spec/modules/yieldliftBidAdapter_spec.js b/test/spec/modules/yieldliftBidAdapter_spec.js index abb5a868c77..c2379ed7778 100644 --- a/test/spec/modules/yieldliftBidAdapter_spec.js +++ b/test/spec/modules/yieldliftBidAdapter_spec.js @@ -191,6 +191,26 @@ describe('YieldLift', function () { expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); expect(payload.regs.ext).to.have.property('gdpr', 1); }); + + it('should properly forward eids parameters', function () { + const req = Object.assign({}, REQUEST); + req.bidRequest[0].userIdAsEids = [ + { + source: 'dummy.com', + uids: [ + { + id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', + atype: 1 + } + ] + }]; + let request = spec.buildRequests(req.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); + expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); + expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index 66e11b9a472..a6f8e7ae891 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -5,10 +5,14 @@ import { nativeBidIsValid, getAssetMessage, getAllAssetsMessage, - decorateAdUnitsWithNativeParams + decorateAdUnitsWithNativeParams, + isOpenRTBBidRequestValid, + isNativeOpenRTBBidValid, + toOrtbNativeRequest, toOrtbNativeResponse, legacyPropertiesToOrtbNative, fireImpressionTrackers, fireClickTrackers, } from 'src/native.js'; import CONSTANTS from 'src/constants.json'; -import {stubAuctionIndex} from '../helpers/indexStub.js'; +import { stubAuctionIndex } from '../helpers/indexStub.js'; +import { convertOrtbRequestToProprietaryNative, fromOrtbNativeRequest } from '../../src/native.js'; const utils = require('src/utils'); const bid = { @@ -21,23 +25,23 @@ const bid = { image: { url: 'http://cdn.example.com/p/creative-image/image.png', height: 83, - width: 127 + width: 127, }, icon: { url: 'http://cdn.example.com/p/creative-image/icon.jpg', height: 742, - width: 989 + width: 989, }, sponsoredBy: 'AppNexus', clickUrl: 'https://www.link.example', clickTrackers: ['https://tracker.example'], impressionTrackers: ['https://impression.example'], - javascriptTrackers: '', + javascriptTrackers: '', ext: { foo: 'foo-value', - baz: 'baz-value' - } - } + baz: 'baz-value', + }, + }, }; const bidWithUndefinedFields = { @@ -50,12 +54,12 @@ const bidWithUndefinedFields = { clickUrl: 'https://www.link.example', clickTrackers: ['https://tracker.example'], impressionTrackers: ['https://impression.example'], - javascriptTrackers: '', + javascriptTrackers: '', ext: { foo: 'foo-value', - baz: undefined - } - } + baz: undefined, + }, + }, }; describe('native.js', function () { @@ -80,7 +84,9 @@ describe('native.js', function () { const targeting = getNativeTargeting(bid); expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal(bid.native.body); - expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal(bid.native.clickUrl); + expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal( + bid.native.clickUrl + ); expect(targeting.hb_native_foo).to.equal(bid.native.foo); }); @@ -92,19 +98,23 @@ describe('native.js', function () { clickUrl: { sendId: true }, ext: { foo: { - sendId: false + sendId: false, }, baz: { - sendId: true - } - } - } + sendId: true, + }, + }, + }, }; const targeting = getNativeTargeting(bid, deps(adUnit)); expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); - expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal('hb_native_body:123'); - expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal('hb_native_linkurl:123'); + expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal( + 'hb_native_body:123' + ); + expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal( + 'hb_native_linkurl:123' + ); expect(targeting.hb_native_foo).to.equal(bid.native.ext.foo); expect(targeting.hb_native_baz).to.equal('hb_native_baz:123'); }); @@ -117,13 +127,13 @@ describe('native.js', function () { clickUrl: { sendId: true }, ext: { foo: { - required: false + required: false, }, baz: { - required: false - } - } - } + required: false, + }, + }, + }, }; const targeting = getNativeTargeting(bidWithUndefinedFields, deps(adUnit)); @@ -132,7 +142,7 @@ describe('native.js', function () { CONSTANTS.NATIVE_KEYS.title, CONSTANTS.NATIVE_KEYS.sponsoredBy, CONSTANTS.NATIVE_KEYS.clickUrl, - 'hb_native_foo' + 'hb_native_foo', ]); }); @@ -142,22 +152,19 @@ describe('native.js', function () { nativeParams: { image: { required: true, - sizes: [150, 50] + sizes: [150, 50], }, title: { required: true, len: 80, - sendTargetingKeys: true + sendTargetingKeys: true, }, sendTargetingKeys: false, - } - + }, }; const targeting = getNativeTargeting(bid, deps(adUnit)); - expect(Object.keys(targeting)).to.deep.equal([ - CONSTANTS.NATIVE_KEYS.title - ]); + expect(Object.keys(targeting)).to.deep.equal([CONSTANTS.NATIVE_KEYS.title]); }); it('should only include targeting if sendTargetingKeys not set to false', function () { @@ -166,38 +173,37 @@ describe('native.js', function () { nativeParams: { image: { required: true, - sizes: [150, 50] + sizes: [150, 50], }, title: { required: true, - len: 80 + len: 80, }, body: { - required: true + required: true, }, clickUrl: { - required: true + required: true, }, icon: { required: false, - sendTargetingKeys: false + sendTargetingKeys: false, }, cta: { required: false, - sendTargetingKeys: false + sendTargetingKeys: false, }, sponsoredBy: { required: false, - sendTargetingKeys: false + sendTargetingKeys: false, }, ext: { foo: { required: false, - sendTargetingKeys: true - } - } - } - + sendTargetingKeys: true, + }, + }, + }, }; const targeting = getNativeTargeting(bid, deps(adUnit)); @@ -206,7 +212,7 @@ describe('native.js', function () { CONSTANTS.NATIVE_KEYS.body, CONSTANTS.NATIVE_KEYS.image, CONSTANTS.NATIVE_KEYS.clickUrl, - 'hb_native_foo' + 'hb_native_foo', ]); }); @@ -216,17 +222,16 @@ describe('native.js', function () { nativeParams: { image: { required: true, - sizes: [150, 50] + sizes: [150, 50], }, title: { required: true, len: 80, }, rendererUrl: { - url: 'https://www.renderer.com/' - } - } - + url: 'https://www.renderer.com/', + }, + }, }; const targeting = getNativeTargeting(bid, deps(adUnit)); @@ -238,7 +243,7 @@ describe('native.js', function () { CONSTANTS.NATIVE_KEYS.icon, CONSTANTS.NATIVE_KEYS.sponsoredBy, CONSTANTS.NATIVE_KEYS.clickUrl, - CONSTANTS.NATIVE_KEYS.rendererUrl + CONSTANTS.NATIVE_KEYS.rendererUrl, ]); expect(bid.native.rendererUrl).to.deep.equal('https://www.renderer.com/'); @@ -251,15 +256,14 @@ describe('native.js', function () { nativeParams: { image: { required: true, - sizes: [150, 50] + sizes: [150, 50], }, title: { required: true, len: 80, }, - adTemplate: '

##hb_native_body##<\/p><\/div>' - } - + adTemplate: '

##hb_native_body##

', + }, }; const targeting = getNativeTargeting(bid, deps(adUnit)); @@ -270,10 +274,12 @@ describe('native.js', function () { CONSTANTS.NATIVE_KEYS.image, CONSTANTS.NATIVE_KEYS.icon, CONSTANTS.NATIVE_KEYS.sponsoredBy, - CONSTANTS.NATIVE_KEYS.clickUrl + CONSTANTS.NATIVE_KEYS.clickUrl, ]); - expect(bid.native.adTemplate).to.deep.equal('

##hb_native_body##<\/p><\/div>'); + expect(bid.native.adTemplate).to.deep.equal( + '

##hb_native_body##

' + ); delete bid.native.adTemplate; }); @@ -281,7 +287,10 @@ describe('native.js', function () { fireNativeTrackers({}, bid); sinon.assert.calledOnce(triggerPixelStub); sinon.assert.calledWith(triggerPixelStub, bid.native.impressionTrackers[0]); - sinon.assert.calledWith(insertHtmlIntoIframeStub, bid.native.javascriptTrackers); + sinon.assert.calledWith( + insertHtmlIntoIframeStub, + bid.native.javascriptTrackers + ); }); it('fires click trackers', function () { @@ -291,7 +300,7 @@ describe('native.js', function () { sinon.assert.calledWith(triggerPixelStub, bid.native.clickTrackers[0]); }); - it('creates native asset message', function() { + it('creates native asset message', function () { const messageRequest = { message: 'Prebid Native', action: 'assetRequest', @@ -304,95 +313,169 @@ describe('native.js', function () { expect(message.assets.length).to.equal(3); expect(message.assets).to.deep.include({ key: 'body', - value: bid.native.body + value: bid.native.body, }); expect(message.assets).to.deep.include({ key: 'image', - value: bid.native.image.url + value: bid.native.image.url, }); expect(message.assets).to.deep.include({ key: 'clickUrl', - value: bid.native.clickUrl + value: bid.native.clickUrl, }); }); - it('creates native all asset message', function() { + it('creates native all asset message', function () { const messageRequest = { message: 'Prebid Native', action: 'allAssetRequest', adId: '123', }; - const message = getAllAssetsMessage(messageRequest, bid); + const message = getAllAssetsMessage(messageRequest, bid, {getNativeReq: () => null}); expect(message.assets.length).to.equal(9); expect(message.assets).to.deep.include({ key: 'body', - value: bid.native.body + value: bid.native.body, }); expect(message.assets).to.deep.include({ key: 'image', - value: bid.native.image.url + value: bid.native.image.url, }); expect(message.assets).to.deep.include({ key: 'clickUrl', - value: bid.native.clickUrl + value: bid.native.clickUrl, }); expect(message.assets).to.deep.include({ key: 'title', - value: bid.native.title + value: bid.native.title, }); expect(message.assets).to.deep.include({ key: 'icon', - value: bid.native.icon.url + value: bid.native.icon.url, }); expect(message.assets).to.deep.include({ key: 'cta', - value: bid.native.cta + value: bid.native.cta, }); expect(message.assets).to.deep.include({ key: 'sponsoredBy', - value: bid.native.sponsoredBy + value: bid.native.sponsoredBy, }); expect(message.assets).to.deep.include({ key: 'foo', - value: bid.native.ext.foo + value: bid.native.ext.foo, }); expect(message.assets).to.deep.include({ key: 'baz', - value: bid.native.ext.baz + value: bid.native.ext.baz, }); }); - it('creates native all asset message with only defined fields', function() { + it('creates native all asset message with only defined fields', function () { const messageRequest = { message: 'Prebid Native', action: 'allAssetRequest', adId: '123', }; - const message = getAllAssetsMessage(messageRequest, bidWithUndefinedFields); + const message = getAllAssetsMessage(messageRequest, bidWithUndefinedFields, {getNativeReq: () => null}); expect(message.assets.length).to.equal(4); expect(message.assets).to.deep.include({ key: 'clickUrl', - value: bid.native.clickUrl + value: bid.native.clickUrl, }); expect(message.assets).to.deep.include({ key: 'title', - value: bid.native.title + value: bid.native.title, }); expect(message.assets).to.deep.include({ key: 'sponsoredBy', - value: bid.native.sponsoredBy + value: bid.native.sponsoredBy, }); expect(message.assets).to.deep.include({ key: 'foo', - value: bid.native.ext.foo + value: bid.native.ext.foo, }); }); }); +describe('validate native openRTB', function () { + it('should validate openRTB request', function () { + let openRTBNativeRequest = { assets: [] }; + // assets array can't be empty + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false); + openRTBNativeRequest.assets.push({ + id: 1.5, + required: 1, + title: {}, + }); + + // asset.id must be integer + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false); + openRTBNativeRequest.assets[0].id = 1; + // title must have 'len' property + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false); + openRTBNativeRequest.assets[0].title.len = 140; + // openRTB request is valid + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(true); + + openRTBNativeRequest.assets.push({ + id: 2, + required: 1, + video: { + mimes: [], + protocols: [], + minduration: 50, + }, + }); + // video asset should have all required properties + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false); + openRTBNativeRequest.assets[1].video.maxduration = 60; + expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(true); + }); + + it('should validate openRTB native bid', function () { + const openRTBRequest = { + assets: [ + { + id: 1, + required: 1, + }, + { + id: 2, + required: 0, + }, + { + id: 3, + required: 1, + }, + ], + }; + let openRTBBid = { + assets: [ + { + id: 1, + }, + { + id: 2, + }, + ], + }; + + // link is missing + expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(false); + openRTBBid.link = { url: 'www.foo.bar' }; + // required id == 3 is missing + expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(false); + + openRTBBid.assets[1].id = 3; + expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(true); + }); +}); + describe('validate native', function () { const adUnit = { transactionId: 'test_adunit', @@ -407,15 +490,15 @@ describe('validate native', function () { image: { required: true, sizes: [150, 50], - aspect_ratios: [150, 50] + aspect_ratios: [150, 50], }, icon: { required: true, - sizes: [50, 50] + sizes: [50, 50], }, - } - } - } + }, + }, + }; let validBid = { adId: 'abc123', @@ -424,23 +507,24 @@ describe('validate native', function () { adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { - body: 'This is a Prebid Native Creative. There are many like it, but this one is mine.', + body: + 'This is a Prebid Native Creative. There are many like it, but this one is mine.', clickTrackers: ['http://my.click.tracker/url'], icon: { url: 'http://my.image.file/ad_image.jpg', height: 75, - width: 75 + width: 75, }, image: { url: 'http://my.icon.file/ad_icon.jpg', height: 2250, - width: 3000 + width: 3000, }, clickUrl: 'http://prebid.org/dev-docs/show-native-ads.html', impressionTrackers: ['http://my.imp.tracker/url'], - javascriptTrackers: '', - title: 'This is an example Prebid Native creative' - } + javascriptTrackers: '', + title: 'This is an example Prebid Native creative', + }, }; let noIconDimBid = { @@ -450,19 +534,20 @@ describe('validate native', function () { adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { - body: 'This is a Prebid Native Creative. There are many like it, but this one is mine.', + body: + 'This is a Prebid Native Creative. There are many like it, but this one is mine.', clickTrackers: ['http://my.click.tracker/url'], icon: 'http://my.image.file/ad_image.jpg', image: { url: 'http://my.icon.file/ad_icon.jpg', height: 2250, - width: 3000 + width: 3000, }, clickUrl: 'http://prebid.org/dev-docs/show-native-ads.html', impressionTrackers: ['http://my.imp.tracker/url'], - javascriptTrackers: '', - title: 'This is an example Prebid Native creative' - } + javascriptTrackers: '', + title: 'This is an example Prebid Native creative', + }, }; let noImgDimBid = { @@ -472,19 +557,20 @@ describe('validate native', function () { adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { - body: 'This is a Prebid Native Creative. There are many like it, but this one is mine.', + body: + 'This is a Prebid Native Creative. There are many like it, but this one is mine.', clickTrackers: ['http://my.click.tracker/url'], icon: { url: 'http://my.image.file/ad_image.jpg', height: 75, - width: 75 + width: 75, }, image: 'http://my.icon.file/ad_icon.jpg', clickUrl: 'http://prebid.org/dev-docs/show-native-ads.html', impressionTrackers: ['http://my.imp.tracker/url'], - javascriptTrackers: '', - title: 'This is an example Prebid Native creative' - } + javascriptTrackers: '', + title: 'This is an example Prebid Native creative', + }, }; beforeEach(function () {}); @@ -493,12 +579,446 @@ describe('validate native', function () { it('should accept bid if no image sizes are defined', function () { decorateAdUnitsWithNativeParams([adUnit]); - const index = stubAuctionIndex({adUnits: [adUnit]}) - let result = nativeBidIsValid(validBid, {index}); + const index = stubAuctionIndex({ adUnits: [adUnit] }); + let result = nativeBidIsValid(validBid, { index }); expect(result).to.be.true; - result = nativeBidIsValid(noIconDimBid, {index}); + result = nativeBidIsValid(noIconDimBid, { index }); expect(result).to.be.true; - result = nativeBidIsValid(noImgDimBid, {index}); + result = nativeBidIsValid(noImgDimBid, { index }); expect(result).to.be.true; }); + + it('should convert from old-style native to OpenRTB request', () => { + const adUnit = { + transactionId: 'test_adunit', + mediaTypes: { + native: { + title: { + required: true, + }, + body: { + required: true, + len: 45 + }, + image: { + required: true, + sizes: [150, 50], + aspect_ratios: [{ + min_width: 150, + min_height: 50 + }] + }, + icon: { + required: true, + aspect_ratios: [{ + min_width: 150, + min_height: 50 + }] + }, + address: {}, + }, + }, + }; + + const ortb = toOrtbNativeRequest(adUnit.mediaTypes.native); + expect(ortb).to.be.a('object'); + expect(ortb.assets).to.be.a('array'); + + // title + expect(ortb.assets[0]).to.deep.include({ + id: 0, + required: 1, + title: { + len: 140 + } + }); + + // body => data + expect(ortb.assets[1]).to.deep.include({ + id: 1, + required: 1, + data: { + type: 2, + len: 45 + } + }); + + // image => image + expect(ortb.assets[2]).to.deep.include({ + id: 2, + required: 1, + img: { + type: 3, // Main Image + w: 150, + h: 50, + } + }); + + expect(ortb.assets[3]).to.deep.include({ + id: 3, + required: 1, + img: { + type: 1, // Icon Image + wmin: 150, + hmin: 50, + } + }); + + expect(ortb.assets[4]).to.deep.include({ + id: 4, + required: 0, + data: { + type: 9, + } + }); + }); + + it('should convert from ortb to old-style native request', () => { + const openRTBRequest = { + 'ver': '1.2', + 'context': 2, + 'contextsubtype': 20, + 'plcmttype': 11, + 'plcmtcnt': 1, + 'aurlsupport': 0, + 'privacy': 1, + 'eventrackers': [ + { + 'event': 1, + 'methods': [1, 2] + }, + { + 'event': 2, + 'methods': [1] + } + ], + 'assets': [ + { + 'id': 123, + 'required': 1, + 'title': { + 'len': 140 + } + }, + { + 'id': 128, + 'required': 0, + 'img': { + 'wmin': 836, + 'hmin': 627, + 'type': 3 + } + }, + { + 'id': 124, + 'required': 1, + 'img': { + 'wmin': 50, + 'hmin': 50, + 'type': 1 + } + }, + { + 'id': 126, + 'required': 1, + 'data': { + 'type': 1, + 'len': 25 + } + }, + { + 'id': 127, + 'required': 1, + 'data': { + 'type': 2, + 'len': 140 + } + } + ] + }; + + const oldNativeRequest = fromOrtbNativeRequest(openRTBRequest); + + expect(oldNativeRequest).to.be.a('object'); + expect(oldNativeRequest.title).to.include({ + required: true, + len: 140 + }); + + expect(oldNativeRequest.image).to.deep.include({ + required: false, + aspect_ratios: { + min_width: 836, + min_height: 627, + ratio_width: 836, + ratio_height: 627 + } + }); + + expect(oldNativeRequest.icon).to.deep.include({ + required: true, + aspect_ratios: { + min_width: 50, + min_height: 50, + ratio_width: 50, + ratio_height: 50 + } + }); + expect(oldNativeRequest.sponsoredBy).to.include({ + required: true, + len: 25 + }) + expect(oldNativeRequest.body).to.include({ + required: true, + len: 140 + }) + }); + + if (FEATURES.NATIVE) { + it('should convert ortb bid requests to proprietary requests', () => { + const validBidRequests = [{ + bidId: 'bidId3', + adUnitCode: 'adUnitCode3', + transactionId: 'transactionId3', + mediaTypes: { + banner: {} + }, + params: { + publisher: 'publisher2', + placement: 'placement3' + } + }]; + const resultRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + expect(resultRequests).to.be.deep.equals(validBidRequests); + + validBidRequests[0].mediaTypes.native = { + ortb: { + ver: '1.2', + context: 2, + contextsubtype: 20, + plcmttype: 11, + plcmtcnt: 1, + aurlsupport: 0, + privacy: 1, + eventrackers: [ + { + event: 1, + methods: [1, 2] + }, + { + event: 2, + methods: [1] + } + ], + assets: [ + { + id: 123, + required: 1, + title: { + len: 140 + } + }, + { + id: 128, + required: 0, + img: { + wmin: 836, + hmin: 627, + type: 3 + } + }, + { + id: 124, + required: 1, + img: { + wmin: 50, + hmin: 50, + type: 1 + } + }, + { + id: 126, + required: 1, + data: { + type: 1, + len: 25 + } + }, + { + id: 127, + required: 1, + data: { + type: 2, + len: 140 + } + } + ] + } + }; + + const resultRequests2 = convertOrtbRequestToProprietaryNative(validBidRequests); + expect(resultRequests2[0].mediaTypes.native).to.deep.include({ + title: { + required: true, + len: 140 + }, + icon: { + required: true, + aspect_ratios: { + min_width: 50, + min_height: 50, + ratio_width: 50, + ratio_height: 50 + } + }, + sponsoredBy: { + required: true, + len: 25 + }, + body: { + required: true, + len: 140 + } + }); + }); + } }); + +describe('legacyPropertiesToOrtbNative', () => { + describe('click trakckers', () => { + it('should convert clickUrl to link.url', () => { + const native = legacyPropertiesToOrtbNative({clickUrl: 'some-url'}); + expect(native.link.url).to.eql('some-url'); + }); + it('should convert single clickTrackers to link.clicktrackers', () => { + const native = legacyPropertiesToOrtbNative({clickTrackers: 'some-url'}); + expect(native.link.clicktrackers).to.eql([ + 'some-url' + ]) + }); + it('should convert multiple clickTrackers into link.clicktrackers', () => { + const native = legacyPropertiesToOrtbNative({clickTrackers: ['url1', 'url2']}); + expect(native.link.clicktrackers).to.eql([ + 'url1', + 'url2' + ]) + }) + }); + describe('impressionTrackers', () => { + it('should convert a single tracker into an eventtracker entry', () => { + const native = legacyPropertiesToOrtbNative({impressionTrackers: 'some-url'}); + expect(native.eventtrackers).to.eql([ + { + event: 1, + method: 1, + url: 'some-url' + } + ]); + }); + + it('should convert an array into corresponding eventtracker entries', () => { + const native = legacyPropertiesToOrtbNative({impressionTrackers: ['url1', 'url2']}); + expect(native.eventtrackers).to.eql([ + { + event: 1, + method: 1, + url: 'url1' + }, + { + event: 1, + method: 1, + url: 'url2' + } + ]) + }) + }); + describe('javascriptTrackers', () => { + it('should convert a single value into jstracker', () => { + const native = legacyPropertiesToOrtbNative({javascriptTrackers: 'some-markup'}); + expect(native.jstracker).to.eql('some-markup'); + }) + it('should merge multiple values into a single jstracker', () => { + const native = legacyPropertiesToOrtbNative({javascriptTrackers: ['some-markup', 'some-other-markup']}); + expect(native.jstracker).to.eql('some-markupsome-other-markup'); + }) + }); +}); + +describe('fireImpressionTrackers', () => { + let runMarkup, fetchURL; + beforeEach(() => { + runMarkup = sinon.stub(); + fetchURL = sinon.stub(); + }) + + function runTrackers(resp) { + fireImpressionTrackers(resp, {runMarkup, fetchURL}) + } + + it('should run markup in jstracker', () => { + runTrackers({ + jstracker: 'some-markup' + }); + sinon.assert.calledWith(runMarkup, 'some-markup'); + }); + + it('should fetch each url in imptrackers', () => { + const urls = ['url1', 'url2']; + runTrackers({ + imptrackers: urls + }); + urls.forEach(url => sinon.assert.calledWith(fetchURL, url)); + }); + + it('should fetch each url in eventtrackers that use the image method', () => { + const urls = ['url1', 'url2']; + runTrackers({ + eventtrackers: urls.map(url => ({event: 1, method: 1, url})) + }); + urls.forEach(url => sinon.assert.calledWith(fetchURL, url)) + }); + + it('should load as a script each url in eventtrackers that use the js method', () => { + const urls = ['url1', 'url2']; + runTrackers({ + eventtrackers: urls.map(url => ({event: 1, method: 2, url})) + }); + urls.forEach(url => sinon.assert.calledWith(runMarkup, sinon.match(`script async src="${url}"`))) + }); + + it('should not fire trackers that are not impression trakcers', () => { + runTrackers({ + link: { + clicktrackers: ['click-url'] + }, + eventtrackers: [{ + event: 2, // not imp + method: 1, + url: 'some-url' + }] + }); + sinon.assert.notCalled(fetchURL); + sinon.assert.notCalled(runMarkup); + }) +}) + +describe('fireClickTrackers', () => { + let fetchURL; + beforeEach(() => { + fetchURL = sinon.stub(); + }); + + function runTrackers(resp) { + fireClickTrackers(resp, {fetchURL}); + } + + it('should load each URL in link.clicktrackers', () => { + const urls = ['url1', 'url2']; + runTrackers({ + link: { + clicktrackers: urls + } + }); + urls.forEach(url => sinon.assert.calledWith(fetchURL, url)); + }) +}) diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index be68fc03765..c040bba4eec 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -12,6 +12,7 @@ import {hook} from '../../../../src/hook.js'; import {auctionManager} from '../../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../../helpers/indexStub.js'; import { bidderSettings } from '../../../../src/bidderSettings.js'; +import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -39,6 +40,10 @@ function onTimelyResponseStub() { } +before(() => { + hook.ready(); +}); + let wrappedCallback = config.callbackWithBidder(CODE); describe('bidders created by newBidder', function () { @@ -47,10 +52,6 @@ describe('bidders created by newBidder', function () { let addBidResponseStub; let doneStub; - before(() => { - hook.ready(); - }); - beforeEach(function () { spec = { code: CODE, @@ -882,6 +883,7 @@ describe('validate bid response: ', function () { title: {'required': true}, } }] + decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', @@ -923,6 +925,7 @@ describe('validate bid response: ', function () { title: {'required': true}, }, }]; + decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', @@ -952,7 +955,7 @@ describe('validate bid response: ', function () { bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(addBidResponseStub.calledOnce).to.equal(false); - expect(logErrorSpy.callCount).to.equal(1); + expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); }); } diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index dacde5491b7..cb38aed9e47 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -3,7 +3,7 @@ import { getCoreStorageManager, storageCallbacks, getStorageManager, - newStorageManager + newStorageManager, validateStorageEnforcement } from 'src/storageManager.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -55,6 +55,33 @@ describe('storage manager', function() { deviceAccessSpy.restore(); }); + describe(`core storage`, () => { + let storage, validateHook; + + beforeEach(() => { + storage = getCoreStorageManager(); + validateHook = sinon.stub().callsFake(function (next, ...args) { + next.apply(this, args); + }); + validateStorageEnforcement.before(validateHook, 99); + }); + + afterEach(() => { + validateStorageEnforcement.getHooks({hook: validateHook}).remove(); + config.resetConfig(); + }) + + it('should respect (vendorless) consent enforcement', () => { + storage.localStorageIsEnabled(); + expect(validateHook.args[0][1]).to.eql(true); // isVendorless should be set to true + }); + + it('should respect the deviceAccess flag', () => { + config.setConfig({deviceAccess: false}); + expect(storage.localStorageIsEnabled()).to.be.false + }) + }) + describe('localstorage forbidden access in 3rd-party context', function() { let errorLogSpy; let originalLocalStorage; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index ca646743147..3cee2b6b679 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -218,6 +218,51 @@ describe('Unit: Prebid Module', function () { auctionManager.clearAllAuctions(); }); + describe('and global adUnits', () => { + const startingAdUnits = [ + { + code: 'one', + }, + { + code: 'two', + } + ]; + let actualAdUnits, hookRan, done; + + function deferringHook(next, req) { + setTimeout(() => { + actualAdUnits = req.adUnits || $$PREBID_GLOBAL$$.adUnits; + done(); + }); + } + + beforeEach(() => { + $$PREBID_GLOBAL$$.requestBids.before(deferringHook, 99); + $$PREBID_GLOBAL$$.adUnits.splice(0, $$PREBID_GLOBAL$$.adUnits.length, ...startingAdUnits); + hookRan = new Promise((resolve) => { + done = resolve; + }); + }); + + afterEach(() => { + $$PREBID_GLOBAL$$.requestBids.getHooks({hook: deferringHook}).remove(); + $$PREBID_GLOBAL$$.adUnits.splice(0, $$PREBID_GLOBAL$$.adUnits.length); + }) + + Object.entries({ + 'addAdUnits': (g) => g.addAdUnits({code: 'three'}), + 'removeAdUnit': (g) => g.removeAdUnit('one') + }).forEach(([method, op]) => { + it(`once called, should not be affected by ${method}`, () => { + $$PREBID_GLOBAL$$.requestBids({}); + op($$PREBID_GLOBAL$$); + return hookRan.then(() => { + expect(actualAdUnits).to.eql(startingAdUnits); + }) + }); + }); + }); + describe('getAdserverTargetingForAdUnitCodeStr', function () { beforeEach(function () { resetAuction(); @@ -2331,14 +2376,47 @@ describe('Unit: Prebid Module', function () { $$PREBID_GLOBAL$$.requestBids({adUnits}); const spyArgs = adapterManager.callBids.getCall(0); const nativeRequest = spyArgs.args[1][0].bids[0].nativeParams; - expect(nativeRequest).to.deep.equal({ - image: {required: true}, - title: {required: true}, - sponsoredBy: {required: true}, - clickUrl: {required: true}, - body: {required: false}, - icon: {required: false}, - }); + expect(nativeRequest.ortb.assets).to.deep.equal([ + { + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }, + { + required: 1, + id: 2, + title: { + len: 140, + } + }, + { + required: 1, + id: 3, + data: { + type: 1, + } + }, + { + required: 0, + id: 4, + data: { + type: 2, + } + }, + { + required: 0, + id: 5, + img: { + type: 1, + wmin: 20, + hmin: 20, + } + }, + ]); resetAuction(); }); }); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 39fa9b9250f..7d5f9af35dd 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,7 +1,6 @@ import { _sendAdToCreative, getReplier, receiveMessage } from 'src/secureCreatives.js'; -import * as secureCreatives from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; @@ -164,6 +163,7 @@ describe('secureCreatives', () => { stubGetAllAssetsMessage.restore(); stubEmit.restore(); resetAuction(); + adResponse.adId = bidId; }); describe('Prebid Request', function() { @@ -336,60 +336,17 @@ describe('secureCreatives', () => { sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); sinon.assert.calledOnce(ev.source.postMessage); sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.STALE_RENDER); - }); - - it('Prebid native should allow stale rendering without config', function () { - pushBidResponseToAuction({}); - - const data = { - adId: bidId, - message: 'Prebid Native', - action: 'allAssetRequest' - }; - - const ev = makeEvent({ - data: JSON.stringify(data), - source: { - postMessage: sinon.stub() - }, - origin: 'any origin' - }); - - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.STALE_RENDER); - - resetHistories(ev.source.postMessage); - - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.BID_WON, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.STALE_RENDER); }); - it('Prebid native should allow stale rendering with config', function () { - configObj.setConfig({'auctionOptions': {'suppressStaleRender': true}}); - - pushBidResponseToAuction({}); + it('Prebid native should not fire BID_WON when receiveMessage is called more than once', () => { + let adId = 3; + pushBidResponseToAuction({ adId }); const data = { - adId: bidId, + adId: adId, message: 'Prebid Native', action: 'allAssetRequest' }; @@ -403,37 +360,18 @@ describe('secureCreatives', () => { }); receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.STALE_RENDER); - - resetHistories(ev.source.postMessage); + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.BID_WON, adResponse); receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.STALE_RENDER); - - configObj.setConfig({'auctionOptions': {}}); + stubEmit.withArgs(CONSTANTS.EVENTS.BID_WON, adResponse).calledOnce; }); it('Prebid native should fire trackers', function () { - pushBidResponseToAuction({}); + let adId = 2; + pushBidResponseToAuction({adId}); const data = { - adId: bidId, + adId: adId, message: 'Prebid Native', action: 'click', }; @@ -450,8 +388,8 @@ describe('secureCreatives', () => { sinon.assert.neverCalledWith(spyLogWarn, warning); sinon.assert.calledOnce(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.BID_WON, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); resetHistories(ev.source.postMessage); @@ -461,8 +399,8 @@ describe('secureCreatives', () => { sinon.assert.neverCalledWith(spyLogWarn, warning); sinon.assert.calledOnce(stubFireNativeTrackers); - sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.BID_WON, adResponse); - sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); + sinon.assert.notCalled(spyAddWinningBid); expect(adResponse).to.have.property('status', CONSTANTS.BID_STATUS.RENDERED); }); diff --git a/test/test_deps.js b/test/test_deps.js index 77fbad93e1c..35713106f8c 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -4,6 +4,7 @@ window.process = { } }; +require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); require('test/mocks/xhr.js');