diff --git a/modules/adpod.js b/modules/adpod.js
new file mode 100644
index 00000000000..87e5869e17b
--- /dev/null
+++ b/modules/adpod.js
@@ -0,0 +1,414 @@
+/**
+ * This module houses the functionality to evaluate and process adpod adunits/bids. Specifically there are several hooked functions,
+ * that either supplement the base function (ie to check something additional or unique to adpod objects) or to replace the base funtion
+ * entirely when appropriate.
+ *
+ * Brief outline of each hook:
+ * - `callPrebidCacheHook` - for any adpod bids, this function will temporarily hold them in a queue in order to send the bids to Prebid Cache in bulk
+ * - `checkAdUnitSetupHook` - evaluates the adUnits to ensure that required fields for adpod adUnits are present. Invalid adpod adUntis are removed from the array.
+ * - `checkVideoBidSetupHook` - evaluates the adpod bid returned from an adaptor/bidder to ensure required fields are populated; also initializes duration bucket field.
+ *
+ * To initialize the module, there is an `initAdpodHooks()` function that should be imported and executed by a corresponding `...AdServerVideo`
+ * module that designed to support adpod video type ads. This import process allows this module to effectively act as a sub-module.
+ */
+
+import * as utils from '../src/utils';
+import { addBidToAuction, doCallbacksIfTimedout, AUCTION_IN_PROGRESS, callPrebidCache } from '../src/auction';
+import { checkAdUnitSetup } from '../src/prebid';
+import { checkVideoBidSetup } from '../src/video';
+import { setupBeforeHookFnOnce } from '../src/hook';
+import { store } from '../src/videoCache';
+import { config } from '../src/config';
+import { ADPOD } from '../src/mediaTypes';
+import Set from 'core-js/library/fn/set';
+import find from 'core-js/library/fn/array/find';
+const from = require('core-js/library/fn/array/from');
+
+export const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur';
+export const TARGETING_KEY_CACHE_ID = 'hb_cache_id'
+
+let queueTimeDelay = 50;
+let queueSizeLimit = 5;
+let bidCacheRegistry = createBidCacheRegistry();
+
+/**
+ * Create a registry object that stores/manages bids while be held in queue for Prebid Cache.
+ * @returns registry object with defined accessor functions
+ */
+function createBidCacheRegistry() {
+ let registry = {};
+
+ function setupRegistrySlot(auctionId) {
+ registry[auctionId] = {};
+ registry[auctionId].bidStorage = new Set();
+ registry[auctionId].queueDispatcher = createDispatcher(queueTimeDelay);
+ registry[auctionId].initialCacheKey = utils.generateUUID();
+ }
+
+ return {
+ addBid: function (bid) {
+ // create parent level object based on auction ID (in case there are concurrent auctions running) to store objects for that auction
+ if (!registry[bid.auctionId]) {
+ setupRegistrySlot(bid.auctionId);
+ }
+ registry[bid.auctionId].bidStorage.add(bid);
+ },
+ removeBid: function (bid) {
+ registry[bid.auctionId].bidStorage.delete(bid);
+ },
+ getBids: function (bid) {
+ return registry[bid.auctionId] && registry[bid.auctionId].bidStorage.values();
+ },
+ getQueueDispatcher: function(bid) {
+ return registry[bid.auctionId] && registry[bid.auctionId].queueDispatcher;
+ },
+ setupInitialCacheKey: function(bid) {
+ if (!registry[bid.auctionId]) {
+ registry[bid.auctionId] = {};
+ registry[bid.auctionId].initialCacheKey = utils.generateUUID();
+ }
+ },
+ getInitialCacheKey: function(bid) {
+ return registry[bid.auctionId] && registry[bid.auctionId].initialCacheKey;
+ }
+ }
+}
+
+/**
+ * Creates a function that when called updates the bid queue and extends the running timer (when called subsequently).
+ * Once the time threshold for the queue (defined by queueSizeLimit) is reached, the queue will be flushed by calling the `firePrebidCacheCall` function.
+ * If there is a long enough time between calls (based on timeoutDration), the queue will automatically flush itself.
+ * @param {Number} timeoutDuration number of milliseconds to pass before timer expires and current bid queue is flushed
+ * @returns {Function}
+ */
+function createDispatcher(timeoutDuration) {
+ let timeout;
+ let counter = 1;
+
+ return function(auctionInstance, bidListArr, afterBidAdded, killQueue) {
+ const context = this;
+
+ var callbackFn = function() {
+ firePrebidCacheCall.call(context, auctionInstance, bidListArr, afterBidAdded);
+ };
+
+ clearTimeout(timeout);
+
+ if (!killQueue) {
+ // want to fire off the queue if either: size limit is reached or time has passed since last call to dispatcher
+ if (counter === queueSizeLimit) {
+ counter = 1;
+ callbackFn();
+ } else {
+ counter++;
+ timeout = setTimeout(callbackFn, timeoutDuration);
+ }
+ } else {
+ counter = 1;
+ }
+ };
+}
+
+/**
+ * This function reads certain fields from the bid to generate a specific key used for caching the bid in Prebid Cache
+ * @param {Object} bid bid object to update
+ * @param {Boolean} brandCategoryExclusion value read from setConfig; influences whether category is required or not
+ */
+function attachPriceIndustryDurationKeyToBid(bid, brandCategoryExclusion) {
+ let initialCacheKey = bidCacheRegistry.getInitialCacheKey(bid);
+ let duration = utils.deepAccess(bid, 'video.durationBucket');
+ let cpmFixed = bid.cpm.toFixed(2);
+ let pcd;
+
+ if (brandCategoryExclusion) {
+ let category = utils.deepAccess(bid, 'meta.adServerCatId');
+ pcd = `${cpmFixed}_${category}_${duration}s`;
+ } else {
+ pcd = `${cpmFixed}_${duration}s`;
+ }
+
+ if (!bid.adserverTargeting) {
+ bid.adserverTargeting = {};
+ }
+ bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] = pcd;
+ bid.adserverTargeting[TARGETING_KEY_CACHE_ID] = initialCacheKey;
+ bid.customCacheKey = `${pcd}_${initialCacheKey}`;
+}
+
+/**
+ * Updates the running queue for the associated auction.
+ * Does a check to ensure the auction is still running; if it's not - the previously running queue is killed.
+ * @param {*} auctionInstance running context of the auction
+ * @param {Object} bidResponse bid object being added to queue
+ * @param {Function} afterBidAdded callback function used when Prebid Cache responds
+ */
+function updateBidQueue(auctionInstance, bidResponse, afterBidAdded) {
+ let bidListIter = bidCacheRegistry.getBids(bidResponse);
+
+ if (bidListIter) {
+ let bidListArr = from(bidListIter);
+ let callDispatcher = bidCacheRegistry.getQueueDispatcher(bidResponse);
+ let killQueue = !!(auctionInstance.getAuctionStatus() !== AUCTION_IN_PROGRESS);
+ callDispatcher(auctionInstance, bidListArr, afterBidAdded, killQueue);
+ } else {
+ utils.logWarn('Attempted to cache a bid from an unknown auction. Bid:', bidResponse);
+ }
+}
+
+/**
+ * Small helper function to remove bids from internal storage; normally b/c they're about to sent to Prebid Cache for processing.
+ * @param {Array[Object]} bidResponses list of bids to remove
+ */
+function removeBidsFromStorage(bidResponses) {
+ for (let i = 0; i < bidResponses.length; i++) {
+ bidCacheRegistry.removeBid(bidResponses[i]);
+ }
+}
+
+/**
+ * This function will send a list of bids to Prebid Cache. It also removes the same bids from the internal bidCacheRegistry
+ * to maintain which bids are in queue.
+ * If the bids are successfully cached, they will be added to the respective auction.
+ * @param {*} auctionInstance running context of the auction
+ * @param {Array[Object]} bidList list of bid objects that need to be sent to Prebid Cache
+ * @param {Function} afterBidAdded callback function used when Prebid Cache responds
+ */
+function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) {
+ // remove entries now so other incoming bids won't accidentally have a stale version of the list while PBC is processing the current submitted list
+ removeBidsFromStorage(bidList);
+
+ store(bidList, function (error, cacheIds) {
+ if (error) {
+ utils.logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`);
+ for (let i = 0; i < bidList.length; i++) {
+ doCallbacksIfTimedout(auctionInstance, bidList[i]);
+ }
+ } else {
+ for (let i = 0; i < cacheIds.length; i++) {
+ // when uuid in response is empty string then the key already existed, so this bid wasn't cached
+ if (cacheIds[i].uuid !== '') {
+ addBidToAuction(auctionInstance, bidList[i]);
+ } else {
+ utils.logInfo(`Detected a bid was not cached because the custom key was already registered. Attempted to use key: ${bidList[i].customCacheKey}. Bid was: `, bidList[i]);
+ }
+ afterBidAdded();
+ }
+ }
+ });
+}
+
+/**
+ * This is the main hook function to handle adpod bids; maintains the logic to temporarily hold bids in a queue in order to send bulk requests to Prebid Cache.
+ * @param {Function} fn reference to original function (used by hook logic)
+ * @param {*} auctionInstance running context of the auction
+ * @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function. If not adpod, returns to original function.
+ * @param {Function} afterBidAdded callback function used when Prebid Cache responds
+ * @param {Object} bidderRequest copy of bid's associated bidderRequest object
+ */
+export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, bidderRequest) {
+ let videoConfig = utils.deepAccess(bidderRequest, 'mediaTypes.video');
+ if (videoConfig && videoConfig.context === ADPOD) {
+ let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
+ let adServerCatId = utils.deepAccess(bidResponse, 'meta.adServerCatId');
+ if (!adServerCatId && brandCategoryExclusion) {
+ utils.logWarn('Detected a bid without meta.adServerCatId while setConfig({adpod.brandCategoryExclusion}) was enabled. This bid has been rejected:', bidResponse)
+ afterBidAdded();
+ }
+
+ if (config.getConfig('adpod.deferCaching') === false) {
+ bidCacheRegistry.addBid(bidResponse);
+ attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
+
+ updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
+ } else {
+ // generate targeting keys for bid
+ bidCacheRegistry.setupInitialCacheKey(bidResponse);
+ attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
+
+ // add bid to auction
+ addBidToAuction(auctionInstance, bidResponse);
+ afterBidAdded();
+ }
+ } else {
+ fn.call(this, auctionInstance, bidResponse, afterBidAdded, bidderRequest);
+ }
+}
+
+/**
+ * This hook function will review the adUnit setup and verify certain required values are present in any adpod adUnits.
+ * If the fields are missing or incorrectly setup, the adUnit is removed from the list.
+ * @param {Function} fn reference to original function (used by hook logic)
+ * @param {Array[Object]} adUnits list of adUnits to be evaluated
+ * @returns {Array[Object]} list of adUnits that passed the check
+ */
+export function checkAdUnitSetupHook(fn, adUnits) {
+ let goodAdUnits = adUnits.filter(adUnit => {
+ let mediaTypes = utils.deepAccess(adUnit, 'mediaTypes');
+ let videoConfig = utils.deepAccess(mediaTypes, 'video');
+ if (videoConfig && videoConfig.context === ADPOD) {
+ // run check to see if other mediaTypes are defined (ie multi-format); reject adUnit if so
+ if (Object.keys(mediaTypes).length > 1) {
+ utils.logWarn(`Detected more than one mediaType in adUnitCode: ${adUnit.code} while attempting to define an 'adpod' video adUnit. 'adpod' adUnits cannot be mixed with other mediaTypes. This adUnit will be removed from the auction.`);
+ return false;
+ }
+
+ let errMsg = `Detected missing or incorrectly setup fields for an adpod adUnit. Please review the following fields of adUnitCode: ${adUnit.code}. This adUnit will be removed from the auction.`;
+
+ let playerSize = !!(videoConfig.playerSize && utils.isArrayOfNums(videoConfig.playerSize));
+ let adPodDurationSec = !!(videoConfig.adPodDurationSec && utils.isNumber(videoConfig.adPodDurationSec));
+ let durationRangeSec = !!(videoConfig.durationRangeSec && utils.isArrayOfNums(videoConfig.durationRangeSec));
+
+ if (!playerSize || !adPodDurationSec || !durationRangeSec) {
+ errMsg += (!playerSize) ? '\nmediaTypes.video.playerSize' : '';
+ errMsg += (!adPodDurationSec) ? '\nmediaTypes.video.adPodDurationSec' : '';
+ errMsg += (!durationRangeSec) ? '\nmediaTypes.video.durationRangeSec' : '';
+ utils.logWarn(errMsg);
+ return false;
+ }
+ }
+ return true;
+ });
+ adUnits = goodAdUnits;
+ fn.call(this, adUnits);
+}
+
+/**
+ * This check evaluates the incoming bid's `video.durationSeconds` field and tests it against specific logic depending on adUnit config. Summary of logic below:
+ * when adUnit.mediaTypes.video.requireExactDuration is true
+ * - only bids that exactly match those listed values are accepted (don't round at all).
+ * - populate the `bid.video.durationBucket` field with the matching duration value
+ * when adUnit.mediaTypes.video.requireExactDuration is false
+ * - round the duration to the next highest specified duration value based on adunit. If the duration is above a range within a set buffer, that bid falls down into that bucket.
+ * (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s)
+ * - if the bid is above the range of the listed durations (and outside the buffer), reject the bid
+ * - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids
+ * @param {Object} bidderRequest copy of the bidderRequest object associated to bidResponse
+ * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory
+ * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine
+*/
+function checkBidDuration(bidderRequest, bidResponse) {
+ const buffer = 2;
+ let bidDuration = utils.deepAccess(bidResponse, 'video.durationSeconds');
+ let videoConfig = utils.deepAccess(bidderRequest, 'mediaTypes.video');
+ let adUnitRanges = videoConfig.durationRangeSec;
+ adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order
+
+ if (!videoConfig.requireExactDuration) {
+ let max = Math.max(...adUnitRanges);
+ if (bidDuration <= (max + buffer)) {
+ let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration);
+ bidResponse.video.durationBucket = nextHighestRange;
+ } else {
+ utils.logWarn(`Detected a bid with a duration value outside the accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Rejecting bid: `, bidResponse);
+ return false;
+ }
+ } else {
+ if (find(adUnitRanges, range => range === bidDuration)) {
+ bidResponse.video.durationBucket = bidDuration;
+ } else {
+ utils.logWarn(`Detected a bid with a duration value not part of the list of accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Exact match durations must be used for this adUnit. Rejecting bid: `, bidResponse);
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * This hooked function evaluates an adpod bid and determines if the required fields are present.
+ * If it's found to not be an adpod bid, it will return to original function via hook logic
+ * @param {Function} fn reference to original function (used by hook logic)
+ * @param {Object} bid incoming bid object
+ * @param {Object} bidRequest bidRequest object of associated bid
+ * @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function
+ * @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function
+ * @returns {boolean} this return is only used for adpod bids
+ */
+export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, context) {
+ if (context === ADPOD) {
+ let result = true;
+ let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
+ if (brandCategoryExclusion && !utils.deepAccess(bid, 'meta.iabSubCatId')) {
+ result = false;
+ }
+
+ if (utils.deepAccess(bid, 'video')) {
+ if (!utils.deepAccess(bid, 'video.context') || bid.video.context !== ADPOD) {
+ result = false;
+ }
+
+ if (!utils.deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) {
+ result = false;
+ } else {
+ let isBidGood = checkBidDuration(bidRequest, bid);
+ if (!isBidGood) result = false;
+ }
+ }
+
+ if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) {
+ utils.logError(`
+ This bid contains only vastXml and will not work when a prebid cache url is not specified.
+ Try enabling prebid cache with pbjs.setConfig({ cache: {url: "..."} });
+ `);
+ result = false;
+ };
+
+ fn.bail(result);
+ } else {
+ fn.call(this, bid, bidRequest, videoMediaType, context);
+ }
+}
+
+/**
+ * This function reads the (optional) settings for the adpod as set from the setConfig()
+ * @param {Object} config contains the config settings for adpod module
+ */
+export function adpodSetConfig(config) {
+ if (config.bidQueueTimeDelay !== undefined) {
+ if (typeof config.bidQueueTimeDelay === 'number' && config.bidQueueTimeDelay > 0) {
+ queueTimeDelay = config.bidQueueTimeDelay;
+ } else {
+ utils.logWarn(`Detected invalid value for adpod.bidQueueTimeDelay in setConfig; must be a positive number. Using default: ${queueTimeDelay}`)
+ }
+ }
+
+ if (config.bidQueueSizeLimit !== undefined) {
+ if (typeof config.bidQueueSizeLimit === 'number' && config.bidQueueSizeLimit > 0) {
+ queueSizeLimit = config.bidQueueSizeLimit;
+ } else {
+ utils.logWarn(`Detected invalid value for adpod.bidQueueSizeLimit in setConfig; must be a positive number. Using default: ${queueSizeLimit}`)
+ }
+ }
+}
+config.getConfig('adpod', config => adpodSetConfig(config.adpod));
+
+/**
+ * This function initializes the adpod module's hooks. This is called by the corresponding adserver video module.
+ */
+export function initAdpodHooks() {
+ setupBeforeHookFnOnce(callPrebidCache, callPrebidCacheHook);
+ setupBeforeHookFnOnce(checkAdUnitSetup, checkAdUnitSetupHook);
+ setupBeforeHookFnOnce(checkVideoBidSetup, checkVideoBidSetupHook);
+}
+
+/**
+ *
+ * @param {Array[Object]} bids list of 'winning' bids that need to be cached
+ * @param {Function} callback send the cached bids (or error) back to adserverVideoModule for further processing
+ }}
+ */
+export function callPrebidCacheAfterAuction(bids, callback) {
+ // will call PBC here and execute cb param to initialize player code
+ store(bids, function(error, cacheIds) {
+ if (error) {
+ callback(error, null);
+ } else {
+ let successfulCachedBids = [];
+ for (let i = 0; i < cacheIds.length; i++) {
+ if (cacheIds[i] !== '') {
+ successfulCachedBids.push(bids[i]);
+ }
+ }
+ callback(null, successfulCachedBids);
+ }
+ })
+}
diff --git a/src/auction.js b/src/auction.js
index f99d3bf0eb8..e83f7d061af 100644
--- a/src/auction.js
+++ b/src/auction.js
@@ -416,7 +416,7 @@ function tryAddVideoBid(auctionInstance, bidResponse, bidRequests, afterBidAdded
}
}
-const callPrebidCache = hook('async', function(auctionInstance, bidResponse, afterBidAdded, bidderRequest) {
+export const callPrebidCache = hook('async', function(auctionInstance, bidResponse, afterBidAdded, bidderRequest) {
store([bidResponse], function (error, cacheIds) {
if (error) {
utils.logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`);
diff --git a/src/hook.js b/src/hook.js
index d8d52b97dc8..7727155e8aa 100644
--- a/src/hook.js
+++ b/src/hook.js
@@ -6,3 +6,10 @@ export let hook = funHooks({
});
export const getHook = hook.get;
+
+export function setupBeforeHookFnOnce(baseFn, hookFn, priority = 15) {
+ let result = baseFn.getHooks({hook: hookFn});
+ if (result.length === 0) {
+ baseFn.before(hookFn, priority);
+ }
+}
diff --git a/src/prebid.js b/src/prebid.js
index 75f0cd9a6d9..0374649c47c 100644
--- a/src/prebid.js
+++ b/src/prebid.js
@@ -69,7 +69,7 @@ function setRenderSize(doc, width, height) {
}
}
-const checkAdUnitSetup = hook('sync', function (adUnits) {
+export const checkAdUnitSetup = hook('sync', function (adUnits) {
adUnits.forEach((adUnit) => {
const mediaTypes = adUnit.mediaTypes;
const normalizedSize = utils.getAdUnitSizes(adUnit);
diff --git a/src/video.js b/src/video.js
index d5fcadd1f38..9cf25016d46 100644
--- a/src/video.js
+++ b/src/video.js
@@ -42,7 +42,7 @@ export function isValidVideoBid(bid, bidRequests) {
return checkVideoBidSetup(bid, bidRequest, videoMediaType, context);
}
-const checkVideoBidSetup = hook('sync', function(bid, bidRequest, videoMediaType, context) {
+export const checkVideoBidSetup = hook('sync', function(bid, bidRequest, videoMediaType, context) {
if (!bidRequest || (videoMediaType && context !== OUTSTREAM)) {
// xml-only video bids require a prebid cache url
if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) {
diff --git a/test/spec/modules/adpod_spec.js b/test/spec/modules/adpod_spec.js
new file mode 100644
index 00000000000..16e94fd569f
--- /dev/null
+++ b/test/spec/modules/adpod_spec.js
@@ -0,0 +1,982 @@
+import * as utils from 'src/utils';
+import { config } from 'src/config';
+import * as videoCache from 'src/videoCache';
+import * as auction from 'src/auction';
+import { ADPOD } from 'src/mediaTypes';
+
+import { callPrebidCacheHook, checkAdUnitSetupHook, checkVideoBidSetupHook, adpodSetConfig } from 'modules/adpod';
+
+let expect = require('chai').expect;
+
+describe('adpod.js', function () {
+ let logErrorStub;
+ let logWarnStub;
+ let logInfoStub;
+
+ describe('callPrebidCacheHook', function () {
+ let callbackResult;
+ let clock;
+ let addBidToAuctionStub;
+ let doCallbacksIfTimedoutStub;
+ let storeStub;
+ let afterBidAddedSpy;
+ let auctionBids = [];
+
+ let callbackFn = function() {
+ callbackResult = true;
+ };
+
+ let auctionInstance = {
+ getAuctionStatus: function() {
+ return auction.AUCTION_IN_PROGRESS;
+ }
+ }
+
+ const fakeStoreFn = function(bids, callback) {
+ let payload = [];
+ bids.forEach(bid => payload.push({uuid: bid.customCacheKey}));
+ callback(null, payload);
+ };
+
+ beforeEach(function() {
+ callbackResult = null;
+ afterBidAddedSpy = sinon.spy();
+ storeStub = sinon.stub(videoCache, 'store');
+ logWarnStub = sinon.stub(utils, 'logWarn');
+ logInfoStub = sinon.stub(utils, 'logInfo');
+ addBidToAuctionStub = sinon.stub(auction, 'addBidToAuction').callsFake(function (auctionInstance, bid) {
+ auctionBids.push(bid);
+ });
+ doCallbacksIfTimedoutStub = sinon.stub(auction, 'doCallbacksIfTimedout');
+ clock = sinon.useFakeTimers();
+ config.setConfig({
+ cache: {
+ url: 'https://prebid.adnxs.com/pbc/v1/cache'
+ }
+ });
+ });
+
+ afterEach(function() {
+ storeStub.restore();
+ logWarnStub.restore();
+ logInfoStub.restore();
+ addBidToAuctionStub.restore();
+ doCallbacksIfTimedoutStub.restore();
+ clock.restore();
+ config.resetConfig();
+ auctionBids = [];
+ })
+
+ it('should redirect back to the original function if bid is not an adpod video', function () {
+ let bid = {
+ adId: 'testAdId_123',
+ mediaType: 'video'
+ };
+
+ let bidderRequest = {
+ adUnitCode: 'adUnit_123',
+ mediaTypes: {
+ video: {
+ context: 'outstream'
+ }
+ }
+ }
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bid, function () {}, bidderRequest);
+ expect(callbackResult).to.equal(true);
+ });
+
+ it('should immediately add the adpod bid to auction if adpod.deferCaching in config is true', function() {
+ config.setConfig({
+ adpod: {
+ deferCaching: true,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'adId01277',
+ auctionId: 'no_defer_123',
+ mediaType: 'video',
+ cpm: 5,
+ meta: {
+ adServerCatId: 'test'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+
+ let bidResponse2 = {
+ adId: 'adId46547',
+ auctionId: 'no_defer_123',
+ mediaType: 'video',
+ cpm: 12,
+ meta: {
+ adServerCatId: 'value'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+
+ let bidderRequest = {
+ adUnitCode: 'adpod_1',
+ auctionId: 'no_defer_123',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 300,
+ durationRangeSec: [15, 30, 45],
+ requireExactDuration: false
+ }
+ },
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+
+ // check if bid adsereverTargeting is setup
+ expect(callbackResult).to.be.null;
+ expect(storeStub.called).to.equal(false);
+ expect(afterBidAddedSpy.calledTwice).to.equal(true);
+ expect(auctionBids.length).to.equal(2);
+ expect(auctionBids[0].adId).to.equal(bidResponse1.adId);
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^5\.00_test_15s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('5.00_test_15s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ expect(auctionBids[1].adId).to.equal(bidResponse2.adId);
+ expect(auctionBids[1].customCacheKey).to.exist.and.to.match(/^12\.00_value_15s_.*/);
+ expect(auctionBids[1].adserverTargeting.hb_pb_cat_dur).to.equal('12.00_value_15s');
+ expect(auctionBids[1].adserverTargeting.hb_cache_id).to.exist;
+ expect(auctionBids[1].adserverTargeting.hb_cache_id).to.equal(auctionBids[0].adserverTargeting.hb_cache_id);
+ });
+
+ it('should send prebid cache call once bid queue is full', function () {
+ storeStub.callsFake(fakeStoreFn);
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'adId123',
+ auctionId: 'full_abc123',
+ mediaType: 'video',
+ cpm: 10,
+ meta: {
+ adServerCatId: 'airline'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 20,
+ durationBucket: 30
+ }
+ };
+ let bidResponse2 = {
+ adId: 'adId234',
+ auctionId: 'full_abc123',
+ mediaType: 'video',
+ cpm: 15,
+ meta: {
+ adServerCatId: 'airline'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 25,
+ durationBucket: 30
+ }
+ };
+ let bidderRequest = {
+ adUnitCode: 'adpod_1',
+ auctionId: 'full_abc123',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 120,
+ durationRangeSec: [15, 30],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledTwice).to.equal(true);
+ expect(auctionBids.length).to.equal(2);
+ expect(auctionBids[0].adId).to.equal('adId123');
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^10\.00_airline_30s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('10.00_airline_30s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ expect(auctionBids[1].adId).to.equal('adId234');
+ expect(auctionBids[1].customCacheKey).to.exist.and.to.match(/^15\.00_airline_30s_.*/);
+ expect(auctionBids[1].adserverTargeting.hb_pb_cat_dur).to.equal('15.00_airline_30s');
+ expect(auctionBids[1].adserverTargeting.hb_cache_id).to.exist;
+ });
+
+ it('should send prebid cache call after set period of time (even if queue is not full)', function () {
+ storeStub.callsFake(fakeStoreFn);
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ bidQueueTimeDelay: 30,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse = {
+ adId: 'adId234',
+ auctionId: 'timer_abc234',
+ mediaType: 'video',
+ cpm: 15,
+ meta: {
+ adServerCatId: 'airline'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 30,
+ durationBucket: 30
+ }
+ };
+ let bidderRequest = {
+ adUnitCode: 'adpod_2',
+ auctionId: 'timer_abc234',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 120,
+ durationRangeSec: [15, 30],
+ requireExactDuration: true
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse, afterBidAddedSpy, bidderRequest);
+ clock.tick(31);
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledOnce).to.equal(true);
+ expect(auctionBids.length).to.equal(1);
+ expect(auctionBids[0].adId).to.equal('adId234');
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^15\.00_airline_30s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('15.00_airline_30s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ });
+
+ it('should execute multiple prebid cache calls when number of bids exceeds queue size', function () {
+ storeStub.callsFake(fakeStoreFn);
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ bidQueueTimeDelay: 30,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'multi_ad1',
+ auctionId: 'multi_call_abc345',
+ mediaType: 'video',
+ cpm: 15,
+ meta: {
+ adServerCatId: 'airline'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+ let bidResponse2 = {
+ adId: 'multi_ad2',
+ auctionId: 'multi_call_abc345',
+ mediaType: 'video',
+ cpm: 15,
+ meta: {
+ adServerCatId: 'news'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+ let bidResponse3 = {
+ adId: 'multi_ad3',
+ auctionId: 'multi_call_abc345',
+ mediaType: 'video',
+ cpm: 10,
+ meta: {
+ adServerCatId: 'sports'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+
+ let bidderRequest = {
+ adUnitCode: 'adpod_3',
+ auctionId: 'multi_call_abc345',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 45,
+ durationRangeSec: [15, 30],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse3, afterBidAddedSpy, bidderRequest);
+ clock.next();
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledThrice).to.equal(true);
+ expect(storeStub.calledTwice).to.equal(true);
+ expect(auctionBids.length).to.equal(3);
+ expect(auctionBids[0].adId).to.equal('multi_ad1');
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^15\.00_airline_15s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('15.00_airline_15s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ expect(auctionBids[1].adId).to.equal('multi_ad2');
+ expect(auctionBids[1].customCacheKey).to.exist.and.to.match(/^15\.00_news_15s_.*/);
+ expect(auctionBids[1].adserverTargeting.hb_pb_cat_dur).to.equal('15.00_news_15s');
+ expect(auctionBids[1].adserverTargeting.hb_cache_id).to.exist.and.to.equal(auctionBids[0].adserverTargeting.hb_cache_id);
+ expect(auctionBids[2].adId).to.equal('multi_ad3');
+ expect(auctionBids[2].customCacheKey).to.exist.and.to.match(/^10\.00_sports_15s_.*/);
+ expect(auctionBids[2].adserverTargeting.hb_pb_cat_dur).to.equal('10.00_sports_15s');
+ expect(auctionBids[2].adserverTargeting.hb_cache_id).to.exist.and.to.equal(auctionBids[0].adserverTargeting.hb_cache_id);
+ });
+
+ it('should cache the bids with a shortened custom key when adpod.brandCategoryExclusion is false', function() {
+ storeStub.callsFake(fakeStoreFn);
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ bidQueueTimeDelay: 30,
+ deferCaching: false,
+ brandCategoryExclusion: false
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'nocat_ad1',
+ auctionId: 'no_category_abc345',
+ mediaType: 'video',
+ cpm: 10,
+ meta: {
+ adServerCatId: undefined
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+ let bidResponse2 = {
+ adId: 'nocat_ad2',
+ auctionId: 'no_category_abc345',
+ mediaType: 'video',
+ cpm: 15,
+ meta: {
+ adServerCatId: undefined
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+
+ let bidderRequest = {
+ adUnitCode: 'adpod_4',
+ auctionId: 'no_category_abc345',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 45,
+ durationRangeSec: [15, 30],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledTwice).to.equal(true);
+ expect(storeStub.calledOnce).to.equal(true);
+ expect(auctionBids.length).to.equal(2);
+ expect(auctionBids[0].adId).to.equal('nocat_ad1');
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^10\.00_15s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('10.00_15s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ expect(auctionBids[1].adId).to.equal('nocat_ad2');
+ expect(auctionBids[1].customCacheKey).to.exist.and.to.match(/^15\.00_15s_.*/);
+ expect(auctionBids[1].adserverTargeting.hb_pb_cat_dur).to.equal('15.00_15s');
+ expect(auctionBids[1].adserverTargeting.hb_cache_id).to.exist.and.to.equal(auctionBids[0].adserverTargeting.hb_cache_id);
+ });
+
+ it('should not add bid to auction when config adpod.brandCategoryExclusion is true but bid is missing adServerCatId', function() {
+ storeStub.callsFake(fakeStoreFn);
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ bidQueueTimeDelay: 30,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'missingCat_ad1',
+ auctionId: 'missing_category_abc345',
+ mediaType: 'video',
+ cpm: 10,
+ meta: {
+ adServerCatId: undefined
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ }
+ };
+
+ let bidderRequest = {
+ adUnitCode: 'adpod_5',
+ auctionId: 'missing_category_abc345',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 45,
+ durationRangeSec: [15, 30],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledOnce).to.equal(true);
+ expect(storeStub.called).to.equal(false);
+ expect(logWarnStub.calledOnce).to.equal(true);
+ expect(auctionBids.length).to.equal(0);
+ });
+
+ it('should not add bid to auction when Prebid Cache detects an existing key', function () {
+ storeStub.callsFake(function(bids, callback) {
+ let payload = [];
+ bids.forEach(bid => payload.push({uuid: bid.customCacheKey}));
+
+ // fake a duplicate bid response from PBC (sets an empty string for the uuid)
+ payload[1].uuid = '';
+ callback(null, payload);
+ });
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'dup_ad_1',
+ auctionId: 'duplicate_def123',
+ mediaType: 'video',
+ cpm: 5,
+ meta: {
+ adServerCatId: 'tech'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 45,
+ durationBucket: 45
+ }
+ };
+ let bidResponse2 = {
+ adId: 'dup_ad_2',
+ auctionId: 'duplicate_def123',
+ mediaType: 'video',
+ cpm: 5,
+ meta: {
+ adServerCatId: 'tech'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 45,
+ durationBucket: 45
+ }
+ };
+ let bidderRequest = {
+ adUnitCode: 'adpod_4',
+ auctionId: 'duplicate_def123',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 120,
+ durationRangeSec: [15, 30, 45],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+
+ expect(callbackResult).to.be.null;
+ expect(afterBidAddedSpy.calledTwice).to.equal(true);
+ expect(storeStub.calledOnce).to.equal(true);
+ expect(logInfoStub.calledOnce).to.equal(true);
+ expect(auctionBids.length).to.equal(1);
+ expect(auctionBids[0].adId).to.equal('dup_ad_1');
+ expect(auctionBids[0].customCacheKey).to.exist.and.to.match(/^5\.00_tech_45s_.*/);
+ expect(auctionBids[0].adserverTargeting.hb_pb_cat_dur).to.equal('5.00_tech_45s');
+ expect(auctionBids[0].adserverTargeting.hb_cache_id).to.exist;
+ });
+
+ it('should not add bids to auction if PBC returns an error', function() {
+ storeStub.callsFake(function(bids, callback) {
+ let payload = [];
+ let errmsg = 'invalid json';
+
+ callback(errmsg, payload);
+ });
+
+ config.setConfig({
+ adpod: {
+ bidQueueSizeLimit: 2,
+ deferCaching: false,
+ brandCategoryExclusion: true
+ }
+ });
+
+ let bidResponse1 = {
+ adId: 'err_ad_1',
+ auctionId: 'error_xyz123',
+ mediaType: 'video',
+ cpm: 5,
+ meta: {
+ adServerCatId: 'tech'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 30,
+ durationBucket: 30
+ }
+ };
+ let bidResponse2 = {
+ adId: 'err_ad_2',
+ auctionId: 'error_xyz123',
+ mediaType: 'video',
+ cpm: 5,
+ meta: {
+ adServerCatId: 'tech'
+ },
+ video: {
+ context: ADPOD,
+ durationSeconds: 30,
+ durationBucket: 30
+ }
+ };
+ let bidderRequest = {
+ adUnitCode: 'adpod_5',
+ auctionId: 'error_xyz123',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 120,
+ durationRangeSec: [15, 30, 45],
+ requireExactDuration: false
+ }
+ }
+ };
+
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, bidderRequest);
+ callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, bidderRequest);
+
+ expect(doCallbacksIfTimedoutStub.calledTwice).to.equal(true);
+ expect(logWarnStub.calledOnce).to.equal(true);
+ expect(auctionBids.length).to.equal(0);
+ });
+ });
+
+ describe('checkAdUnitSetupHook', function () {
+ let results;
+ let callbackFn = function (adUnits) {
+ results = adUnits;
+ };
+
+ beforeEach(function () {
+ logWarnStub = sinon.stub(utils, 'logWarn');
+ results = null;
+ });
+
+ afterEach(function() {
+ utils.logWarn.restore();
+ });
+
+ it('removes an incorrectly setup adpod adunit - required fields are missing', function() {
+ let adUnits = [{
+ code: 'test1',
+ mediaTypes: {
+ video: {
+ context: ADPOD
+ }
+ }
+ }, {
+ code: 'test2',
+ mediaTypes: {
+ video: {
+ context: 'instream'
+ }
+ }
+ }];
+
+ checkAdUnitSetupHook(callbackFn, adUnits);
+
+ expect(results).to.deep.equal([{
+ code: 'test2',
+ mediaTypes: {
+ video: {
+ context: 'instream'
+ }
+ }
+ }]);
+ expect(logWarnStub.calledOnce).to.equal(true);
+ });
+
+ it('removes an incorrectly setup adpod adunit - attempting to use multi-format adUnit', function() {
+ let adUnits = [{
+ code: 'multi_test1',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300, 600]]
+ },
+ video: {
+ context: 'adpod',
+ playerSize: [300, 250],
+ durationRangeSec: [15, 30, 45],
+ adPodDurationSec: 300
+ }
+ }
+ }];
+
+ checkAdUnitSetupHook(callbackFn, adUnits);
+
+ expect(results).to.deep.equal([]);
+ expect(logWarnStub.calledOnce).to.equal(true);
+ });
+
+ it('accepts mixed set of adunits', function() {
+ let adUnits = [{
+ code: 'test3',
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 300],
+ adPodDurationSec: 360,
+ durationRangeSec: [15, 30, 45],
+ requireExactDuration: true
+ }
+ }
+ }, {
+ code: 'test4',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]]
+ }
+ }
+ }];
+
+ checkAdUnitSetupHook(callbackFn, adUnits);
+
+ expect(results).to.deep.equal(adUnits);
+ expect(logWarnStub.called).to.equal(false);
+ });
+ });
+
+ describe('checkVideoBidSetupHook', function () {
+ let callbackResult;
+ let bailResult;
+ const callbackFn = {
+ call: function(context, bid) {
+ callbackResult = bid;
+ },
+ bail: function(result) {
+ bailResult = result;
+ }
+ }
+ const adpodTestBid = {
+ video: {
+ context: ADPOD,
+ durationSeconds: 15,
+ durationBucket: 15
+ },
+ meta: {
+ iabSubCatId: 'testCategory_123'
+ },
+ vastXml: 'test XML here'
+ };
+ const bidderRequestNoExact = {
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 400],
+ durationRangeSec: [15, 45],
+ requireExactDuration: false,
+ adPodDurationSec: 300
+ }
+ }
+ };
+ const bidderRequestWithExact = {
+ mediaTypes: {
+ video: {
+ context: ADPOD,
+ playerSize: [300, 400],
+ durationRangeSec: [15, 30, 45, 60],
+ requireExactDuration: true,
+ adPodDurationSec: 300
+ }
+ }
+ };
+
+ beforeEach(function() {
+ callbackResult = null;
+ bailResult = null;
+ config.setConfig({
+ cache: {
+ url: 'http://test.cache.url/endpoint'
+ },
+ adpod: {
+ brandCategoryExclusion: true
+ }
+ });
+ logWarnStub = sinon.stub(utils, 'logWarn');
+ logErrorStub = sinon.stub(utils, 'logError');
+ });
+
+ afterEach(function() {
+ config.resetConfig();
+ logWarnStub.restore();
+ logErrorStub.restore();
+ })
+
+ it('redirects to original function for non-adpod type video bids', function() {
+ let bannerTestBid = {
+ mediaType: 'video'
+ };
+ checkVideoBidSetupHook(callbackFn, bannerTestBid, {}, {}, 'instream');
+ expect(callbackResult).to.deep.equal(bannerTestBid);
+ expect(bailResult).to.be.null;
+ expect(logErrorStub.called).to.equal(false);
+ });
+
+ it('returns true when adpod bid is properly setup', function() {
+ config.setConfig({
+ cache: {
+ url: 'http://test.cache.url/endpoint'
+ },
+ adpod: {
+ brandCategoryExclusion: false
+ }
+ });
+
+ let goodBid = utils.deepClone(adpodTestBid);
+ goodBid.meta.iabSubCatId = undefined;
+ checkVideoBidSetupHook(callbackFn, goodBid, bidderRequestNoExact, {}, ADPOD);
+ expect(callbackResult).to.be.null;
+ expect(bailResult).to.equal(true);
+ expect(logErrorStub.called).to.equal(false);
+ });
+
+ it('returns true when adpod bid is missing iab category while brandCategoryExclusion in config is false', function() {
+ let goodBid = utils.deepClone(adpodTestBid);
+ checkVideoBidSetupHook(callbackFn, goodBid, bidderRequestNoExact, {}, ADPOD);
+ expect(callbackResult).to.be.null;
+ expect(bailResult).to.equal(true);
+ expect(logErrorStub.called).to.equal(false);
+ });
+
+ it('returns false when a required property from an adpod bid is missing', function() {
+ function testInvalidAdpodBid(badTestBid, shouldErrorBeLogged) {
+ checkVideoBidSetupHook(callbackFn, badTestBid, bidderRequestNoExact, {}, ADPOD);
+ expect(callbackResult).to.be.null;
+ expect(bailResult).to.equal(false);
+ expect(logErrorStub.called).to.equal(shouldErrorBeLogged);
+ }
+
+ let noCatBid = utils.deepClone(adpodTestBid);
+ noCatBid.meta.iabSubCatId = undefined;
+ testInvalidAdpodBid(noCatBid, false);
+
+ let noContextBid = utils.deepClone(adpodTestBid);
+ delete noContextBid.video.context;
+ testInvalidAdpodBid(noContextBid, false);
+
+ let wrongContextBid = utils.deepClone(adpodTestBid);
+ wrongContextBid.video.context = 'instream';
+ testInvalidAdpodBid(wrongContextBid, false);
+
+ let noDurationBid = utils.deepClone(adpodTestBid);
+ delete noDurationBid.video.durationSeconds;
+ testInvalidAdpodBid(noDurationBid, false);
+
+ config.resetConfig();
+ let noCacheUrlBid = utils.deepClone(adpodTestBid);
+ testInvalidAdpodBid(noCacheUrlBid, true);
+ });
+
+ describe('checkBidDuration', function() {
+ const basicBid = {
+ video: {
+ context: ADPOD,
+ durationSeconds: 30
+ },
+ meta: {
+ iabSubCatId: 'testCategory_123'
+ },
+ vastXml: ''
+ };
+
+ it('when requireExactDuration is true', function() {
+ let goodBid = utils.deepClone(basicBid);
+ checkVideoBidSetupHook(callbackFn, goodBid, bidderRequestWithExact, {}, ADPOD);
+
+ expect(callbackResult).to.be.null;
+ expect(goodBid.video.durationBucket).to.equal(30);
+ expect(bailResult).to.equal(true);
+ expect(logWarnStub.called).to.equal(false);
+
+ let badBid = utils.deepClone(basicBid);
+ badBid.video.durationSeconds = 14;
+ checkVideoBidSetupHook(callbackFn, badBid, bidderRequestWithExact, {}, ADPOD);
+
+ expect(callbackResult).to.be.null;
+ expect(badBid.video.durationBucket).to.be.undefined;
+ expect(bailResult).to.equal(false);
+ expect(logWarnStub.calledOnce).to.equal(true);
+ });
+
+ it('when requireExactDuration is false and bids are bucketed properly', function() {
+ function testRoundingForGoodBId(bid, bucketValue) {
+ checkVideoBidSetupHook(callbackFn, bid, bidderRequestNoExact, {}, ADPOD);
+ expect(callbackResult).to.be.null;
+ expect(bid.video.durationBucket).to.equal(bucketValue);
+ expect(bailResult).to.equal(true);
+ expect(logWarnStub.called).to.equal(false);
+ }
+
+ let goodBid45 = utils.deepClone(basicBid);
+ goodBid45.video.durationSeconds = 45;
+ testRoundingForGoodBId(goodBid45, 45);
+
+ let goodBid30 = utils.deepClone(basicBid);
+ goodBid30.video.durationSeconds = 30;
+ testRoundingForGoodBId(goodBid30, 45);
+
+ let goodBid10 = utils.deepClone(basicBid);
+ goodBid10.video.durationSeconds = 10;
+ testRoundingForGoodBId(goodBid10, 15);
+
+ let goodBid16 = utils.deepClone(basicBid);
+ goodBid16.video.durationSeconds = 16;
+ testRoundingForGoodBId(goodBid16, 15);
+
+ let goodBid47 = utils.deepClone(basicBid);
+ goodBid47.video.durationSeconds = 47;
+ testRoundingForGoodBId(goodBid47, 45);
+ });
+
+ it('when requireExactDuration is false and bid duration exceeds listed buckets', function() {
+ function testRoundingForBadBid(bid) {
+ checkVideoBidSetupHook(callbackFn, bid, bidderRequestNoExact, {}, ADPOD);
+ expect(callbackResult).to.be.null;
+ expect(bid.video.durationBucket).to.be.undefined;
+ expect(bailResult).to.equal(false);
+ expect(logWarnStub.called).to.equal(true);
+ }
+
+ let badBid100 = utils.deepClone(basicBid);
+ badBid100.video.durationSeconds = 100;
+ testRoundingForBadBid(badBid100);
+
+ let badBid48 = utils.deepClone(basicBid);
+ badBid48.video.durationSeconds = 48;
+ testRoundingForBadBid(badBid48);
+ });
+ });
+ });
+
+ describe('adpodSetConfig', function () {
+ let logWarnStub;
+ beforeEach(function() {
+ logWarnStub = sinon.stub(utils, 'logWarn');
+ });
+
+ afterEach(function () {
+ logWarnStub.restore();
+ });
+
+ it('should log a warning when values other than numbers are used in setConfig', function() {
+ adpodSetConfig({
+ bidQueueSizeLimit: '2',
+ bidQueueTimeDelay: '50'
+ });
+ expect(logWarnStub.calledTwice).to.equal(true);
+ });
+
+ it('should log a warning when numbers less than or equal to zero are used in setConfig', function() {
+ adpodSetConfig({
+ bidQueueSizeLimit: 0,
+ bidQueueTimeDelay: -2
+ });
+ expect(logWarnStub.calledTwice).to.equal(true);
+ });
+
+ it('should not log any warning when using a valid config', function() {
+ adpodSetConfig({
+ bidQueueSizeLimit: 10
+ });
+ expect(logWarnStub.called).to.equal(false);
+
+ adpodSetConfig({
+ bidQueueTimeDelay: 100,
+ bidQueueSizeLimit: 20
+ });
+ expect(logWarnStub.called).to.equal(false);
+ })
+ });
+});