diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index 0f5e24a301a..aa4bf5ea782 100644 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -12,91 +12,91 @@ --> - - - - - + + + - - - - - - - -

Prebid.js Test

-
Div-1
-
- -
- - + + function sendAdserverRequest() { + if (pbjs.adserverRequestSent) return; + pbjs.adserverRequestSent = true; + googletag.cmd.push(function() { + pbjs.que.push(function() { + pbjs.setTargetingForGPTAsync(); + googletag.pubads().refresh(); + }); + }); + } + + setTimeout(function() { + sendAdserverRequest(); + }, PREBID_TIMEOUT); + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + \ No newline at end of file diff --git a/modules/currency.js b/modules/currency.js index 0c2b87a6bc3..f5da02d5d72 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -3,6 +3,7 @@ import { STATUS } from 'src/constants'; import { ajax } from 'src/ajax'; import * as utils from 'src/utils'; import { config } from 'src/config'; +import { hooks } from 'src/hook.js'; const DEFAULT_CURRENCY_RATE_URL = 'http://currency.prebid.org/latest.json'; const CURRENCY_RATE_PRECISION = 4; @@ -77,6 +78,10 @@ function initCurrency(url) { conversionCache = {}; currencySupportEnabled = true; + utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + + hooks['addBidResponse'].addHook(addBidResponseHook, 100); + if (!currencyRates.conversions) { ajax(url, function (response) { try { @@ -92,6 +97,10 @@ function initCurrency(url) { } function resetCurrency() { + utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); + + hooks['addBidResponse'].removeHook(addBidResponseHook, 100); + adServerCurrency = 'USD'; conversionCache = {}; currencySupportEnabled = false; @@ -100,37 +109,35 @@ function resetCurrency() { bidderCurrencyDefault = {}; } -export function addBidResponseDecorator(fn) { - return function(adUnitCode, bid) { - if (!bid) { - return fn.apply(this, arguments); // if no bid, call original and let it display warnings - } +export function addBidResponseHook(adUnitCode, bid, fn) { + if (!bid) { + return fn.apply(this, arguments); // if no bid, call original and let it display warnings + } - let bidder = bid.bidderCode || bid.bidder; - if (bidderCurrencyDefault[bidder]) { - let currencyDefault = bidderCurrencyDefault[bidder]; - if (bid.currency && currencyDefault !== bid.currency) { - utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); - } else { - bid.currency = currencyDefault; - } + let bidder = bid.bidderCode || bid.bidder; + if (bidderCurrencyDefault[bidder]) { + let currencyDefault = bidderCurrencyDefault[bidder]; + if (bid.currency && currencyDefault !== bid.currency) { + utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); + } else { + bid.currency = currencyDefault; } + } - // default to USD if currency not set - if (!bid.currency) { - utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); - bid.currency = 'USD'; - } + // default to USD if currency not set + if (!bid.currency) { + utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); + bid.currency = 'USD'; + } - // execute immediately if the bid is already in the desired currency - if (bid.currency === adServerCurrency) { - return fn.apply(this, arguments); - } + // execute immediately if the bid is already in the desired currency + if (bid.currency === adServerCurrency) { + return fn.apply(this, arguments); + } - bidResponseQueue.push(wrapFunction(fn, this, arguments)); - if (!currencySupportEnabled || currencyRatesLoaded) { - processBidResponseQueue(); - } + bidResponseQueue.push(wrapFunction(fn, this, arguments)); + if (!currencySupportEnabled || currencyRatesLoaded) { + processBidResponseQueue(); } } diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 09e9eb79514..a1bc701d1e9 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -4,8 +4,10 @@ import { config } from 'src/config'; import bidfactory from 'src/bidfactory'; import { STATUS } from 'src/constants'; import { userSync } from 'src/userSync'; +import { nativeBidIsValid } from 'src/native'; +import { isValidVideoBid } from 'src/video'; -import { logWarn, logError, parseQueryStringParameters, delayExecution } from 'src/utils'; +import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest } from 'src/utils'; /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -160,7 +162,9 @@ export function newBidder(spec) { const adUnitCodesHandled = {}; function addBidWithCode(adUnitCode, bid) { adUnitCodesHandled[adUnitCode] = true; - addBidResponse(adUnitCode, bid); + if (isValid(adUnitCode, bid, [bidderRequest])) { + addBidResponse(adUnitCode, bid); + } } // After all the responses have come back, call done() and @@ -279,17 +283,12 @@ export function newBidder(spec) { onResponse(); function addBidUsingRequestMap(bid) { - // In Prebid 1.0 all the validation logic from bidmanager will move here, as of now we are only validating new params so that adapters dont miss adding them. - if (hasValidKeys(bid)) { - const bidRequest = bidRequestMap[bid.requestId]; - if (bidRequest) { - const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); - addBidWithCode(bidRequest.adUnitCode, prebidBid); - } else { - logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); - } + const bidRequest = bidRequestMap[bid.requestId]; + if (bidRequest) { + const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); + addBidWithCode(bidRequest.adUnitCode, prebidBid); } else { - logError(`Bidder ${spec.code} is missing required params. Check http://prebid.org/dev-docs/bidder-adapter-1.html for list of params.`); + logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); } } @@ -318,8 +317,68 @@ export function newBidder(spec) { return true; } - function hasValidKeys(bid) { - let bidKeys = Object.keys(bid); - return COMMON_BID_RESPONSE_KEYS.every(key => bidKeys.includes(key)); + // Validate the arguments sent to us by the adapter. If this returns false, the bid should be totally ignored. + function isValid(adUnitCode, bid, bidRequests) { + function hasValidKeys() { + let bidKeys = Object.keys(bid); + return COMMON_BID_RESPONSE_KEYS.every(key => bidKeys.includes(key)); + } + + function errorMessage(msg) { + return `Invalid bid from ${bid.bidderCode}. Ignoring bid: ${msg}`; + } + + if (!adUnitCode) { + logWarn('No adUnitCode was supplied to addBidResponse.'); + return false; + } + + if (!bid) { + logWarn(`Some adapter tried to add an undefined bid for ${adUnitCode}.`); + return false; + } + + if (!hasValidKeys()) { + logError(errorMessage(`Bidder ${bid.bidderCode} is missing required params. Check http://prebid.org/dev-docs/bidder-adapter-1.html for list of params.`)); + return false; + } + + if (bid.mediaType === 'native' && !nativeBidIsValid(bid, bidRequests)) { + logError(errorMessage('Native bid missing some required properties.')); + return false; + } + if (bid.mediaType === 'video' && !isValidVideoBid(bid, bidRequests)) { + logError(errorMessage(`Video bid does not have required vastUrl or renderer property`)); + return false; + } + if (bid.mediaType === 'banner' && !validBidSize(adUnitCode, bid, bidRequests)) { + logError(errorMessage(`Banner bids require a width and height`)); + return false; + } + + return true; + } + + // check that the bid has a width and height set + function validBidSize(adUnitCode, bid, bidRequests) { + if ((bid.width || bid.width === 0) && (bid.height || bid.height === 0)) { + return true; + } + + const adUnit = getBidderRequest(bidRequests, bid.bidderCode, adUnitCode); + + const sizes = adUnit && adUnit.bids && adUnit.bids[0] && adUnit.bids[0].sizes; + const parsedSizes = parseSizesInput(sizes); + + // if a banner impression has one valid size, we assign that size to any bid + // response that does not explicitly set width or height + if (parsedSizes.length === 1) { + const [ width, height ] = parsedSizes[0].split('x'); + bid.width = width; + bid.height = height; + return true; + } + + return false; } } diff --git a/src/auction.js b/src/auction.js index 36f1d4ed27b..e9fb0714ace 100644 --- a/src/auction.js +++ b/src/auction.js @@ -50,13 +50,12 @@ import { uniques, timestamp, adUnitsFilter, delayExecution, getBidderRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; -import { NATIVE_KEYS, nativeBidIsValid } from './native'; -import { isValidVideoBid } from './video'; +import { NATIVE_KEYS } from './native'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; import { config } from 'src/config'; import { userSync } from 'src/userSync'; -import { addBidResponseDecorator } from 'modules/currency'; +import { createHook } from 'src/hook'; const { syncUsers } = userSync; const utils = require('./utils'); @@ -112,6 +111,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout}) { if (_callback != null) { try { + _auctionStatus = AUCTION_COMPLETED; const adUnitCodes = _adUnitCodes; const bids = [_bidsReceived .filter(adUnitsFilter.bind(this, adUnitCodes)) @@ -169,171 +169,6 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout}) { }, 1); } - function addBidResponse(adUnitCode, bid) { - if (isValid()) { - prepareBidForAuction(); - - if (bid.mediaType === 'video') { - tryAddVideoBid(bid); - } else { - doCallbacksIfNeeded(); - addBidToAuction(bid); - } - } - - // Validate the arguments sent to us by the adapter. If this returns false, the bid should be totally ignored. - function isValid() { - function errorMessage(msg) { - return `Invalid bid from ${bid.bidderCode}. Ignoring bid: ${msg}`; - } - - if (!adUnitCode) { - utils.logWarn('No adUnitCode was supplied to addBidResponse.'); - return false; - } - - const bidRequest = getBidderRequest(_bidderRequests, bid.bidderCode, adUnitCode); - if (!bidRequest.start) { - utils.logError(errorMessage('Cannot find valid matching bid request.')); - return false; - } - - if (!bid) { - utils.logWarn(`Some adapter tried to add an undefined bid for ${adUnitCode}.`); - return false; - } - if (bid.mediaType === 'native' && !nativeBidIsValid(bid, _bidderRequests)) { - utils.logError(errorMessage('Native bid missing some required properties.')); - return false; - } - if (bid.mediaType === 'video' && !isValidVideoBid(bid, _bidderRequests)) { - utils.logError(errorMessage(`Video bid does not have required vastUrl or renderer property`)); - return false; - } - if (bid.mediaType === 'banner' && !validBidSize(bid)) { - utils.logError(errorMessage(`Banner bids require a width and height`)); - return false; - } - - return true; - } - - // check that the bid has a width and height set - function validBidSize(bid) { - if ((bid.width || bid.width === 0) && (bid.height || bid.height === 0)) { - return true; - } - - const adUnit = getBidderRequest(_bidderRequests, bid.bidderCode, adUnitCode); - - const sizes = adUnit && adUnit.bids && adUnit.bids[0] && adUnit.bids[0].sizes; - const parsedSizes = utils.parseSizesInput(sizes); - - // if a banner impression has one valid size, we assign that size to any bid - // response that does not explicitly set width or height - if (parsedSizes.length === 1) { - const [ width, height ] = parsedSizes[0].split('x'); - bid.width = width; - bid.height = height; - return true; - } - - return false; - } - - // Postprocess the bids so that all the universal properties exist, no matter which bidder they came from. - // This should be called before addBidToAuction(). - function prepareBidForAuction() { - let bidRequest = _bidderRequests.find(request => { - return request.bids - .filter(rbid => rbid.bidder === bid.bidderCode && rbid.adUnitCode === adUnitCode).length > 0; - }) || {start: null}; - - const start = bidRequest.start; - - Object.assign(bid, { - auctionId: _auctionId, - requestId: bidRequest.requestId, - responseTimestamp: timestamp(), - requestTimestamp: start, - cpm: parseFloat(bid.cpm) || 0, - bidder: bid.bidderCode, - adUnitCode - }); - - bid.timeToRespond = bid.responseTimestamp - bid.requestTimestamp; - - // Let listeners know that now is the time to adjust the bid, if they want to. - // - // CAREFUL: Publishers rely on certain bid properties to be available (like cpm), - // but others to not be set yet (like priceStrings). See #1372 and #1389. - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bid); - - // a publisher-defined renderer can be used to render bids - const adUnitRenderer = - bidRequest.bids && bidRequest.bids[0] && bidRequest.bids[0].renderer; - - if (adUnitRenderer) { - bid.renderer = Renderer.install({ url: adUnitRenderer.url }); - bid.renderer.setRender(adUnitRenderer.render); - } - - const priceStringsObj = getPriceBucketString( - bid.cpm, - config.getConfig('customPriceBucket'), - config.getConfig('currency.granularityMultiplier') - ); - bid.pbLg = priceStringsObj.low; - bid.pbMg = priceStringsObj.med; - bid.pbHg = priceStringsObj.high; - bid.pbAg = priceStringsObj.auto; - bid.pbDg = priceStringsObj.dense; - bid.pbCg = priceStringsObj.custom; - - // if there is any key value pairs to map do here - var keyValues; - if (bid.bidderCode && (bid.cpm > 0 || bid.dealId)) { - keyValues = getKeyValueTargetingPairs(bid.bidderCode, bid); - } - - // use any targeting provided as defaults, otherwise just set from getKeyValueTargetingPairs - bid.adserverTargeting = Object.assign(bid.adserverTargeting || {}, keyValues); - } - - function doCallbacksIfNeeded() { - if (bid.timeToRespond > _timeout + config.getConfig('timeoutBuffer')) { - executeCallback(true); - } - } - - // Add a bid to the auction. - function addBidToAuction() { - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bid); - addBidReceived(bid); - } - - // Video bids may fail if the cache is down, or there's trouble on the network. - function tryAddVideoBid(bid) { - if (config.getConfig('usePrebidCache')) { - store([bid], function(error, cacheIds) { - if (error) { - utils.logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - } else { - bid.videoCacheKey = cacheIds[0].uuid; - if (!bid.vastUrl) { - bid.vastUrl = getCacheUrl(bid.videoCacheKey); - } - addBidToAuction(bid); - } - doCallbacksIfNeeded(); - }); - } else { - doCallbacksIfNeeded(); - addBidToAuction(bid); - } - } - } - function callBids() { startAuctionTimer(); _auctionStatus = AUCTION_STARTED; @@ -353,19 +188,128 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout}) { }); _auctionStatus = AUCTION_IN_PROGRESS; - adaptermanager.callBids(_adUnits, bidRequests, addBidResponseDecorator(addBidResponse.bind(this)), done.bind(this)); + adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(this), done.bind(this)); }; return { + addBidReceived, + executeCallback, + callBids, + getTimeout: () => _timeout, + getAuctionId: () => _auctionId, getAuctionStatus: () => _auctionStatus, getAdUnits: () => _adUnits, getAdUnitCodes: () => _adUnitCodes, getBidRequests: () => _bidderRequests, getBidsReceived: () => _bidsReceived, - callBids } } +export const addBidResponse = createHook('asyncSeries', function(adUnitCode, bid) { + let auctionInstance = this; + let bidRequests = auctionInstance.getBidRequests(); + let auctionId = auctionInstance.getAuctionId(); + + let bidResponse = getPreparedBidForAuction({adUnitCode, bid, bidRequests, auctionId}); + if (bidResponse.mediaType === 'video') { + tryAddVideoBid(bidResponse); + } else { + doCallbacksIfNeeded(); + addBidToAuction(bidResponse); + } + + function doCallbacksIfNeeded() { + if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { + auctionInstance.executeCallback(true); + } + } + + // Add a bid to the auction. + function addBidToAuction() { + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); + auctionInstance.addBidReceived(bidResponse); + } + + // Video bids may fail if the cache is down, or there's trouble on the network. + function tryAddVideoBid(bidResponse) { + if (config.getConfig('usePrebidCache')) { + store([bidResponse], function(error, cacheIds) { + if (error) { + utils.logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); + } else { + bidResponse.videoCacheKey = cacheIds[0].uuid; + if (!bid.vastUrl) { + bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); + } + addBidToAuction(bidResponse); + } + doCallbacksIfNeeded(); + }); + } else { + doCallbacksIfNeeded(); + addBidToAuction(bidResponse); + } + } +}, 'addBidResponse'); + +// Postprocess the bids so that all the universal properties exist, no matter which bidder they came from. +// This should be called before addBidToAuction(). +function getPreparedBidForAuction({adUnitCode, bid, bidRequests, auctionId}) { + let bidRequest = getBidderRequest(bidRequests, bid.bidderCode, adUnitCode); + + const start = bidRequest.start; + + let bidObject = Object.assign({}, bid, { + auctionId, + requestId: bidRequest.requestId, + responseTimestamp: timestamp(), + requestTimestamp: start, + cpm: parseFloat(bid.cpm) || 0, + bidder: bid.bidderCode, + adUnitCode + }); + + bidObject.timeToRespond = bidObject.responseTimestamp - bidObject.requestTimestamp; + + // Let listeners know that now is the time to adjust the bid, if they want to. + // + // CAREFUL: Publishers rely on certain bid properties to be available (like cpm), + // but others to not be set yet (like priceStrings). See #1372 and #1389. + events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); + + // a publisher-defined renderer can be used to render bids + const adUnitRenderer = + bidRequest.bids && bidRequest.bids[0] && bidRequest.bids[0].renderer; + + if (adUnitRenderer) { + bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); + bidObject.renderer.setRender(adUnitRenderer.render); + } + + const priceStringsObj = getPriceBucketString( + bidObject.cpm, + config.getConfig('customPriceBucket'), + config.getConfig('currency.granularityMultiplier') + ); + bidObject.pbLg = priceStringsObj.low; + bidObject.pbMg = priceStringsObj.med; + bidObject.pbHg = priceStringsObj.high; + bidObject.pbAg = priceStringsObj.auto; + bidObject.pbDg = priceStringsObj.dense; + bidObject.pbCg = priceStringsObj.custom; + + // if there is any key value pairs to map do here + var keyValues; + if (bidObject.bidderCode && (bidObject.cpm > 0 || bidObject.dealId)) { + keyValues = getKeyValueTargetingPairs(bidObject.bidderCode, bidObject); + } + + // use any targeting provided as defaults, otherwise just set from getKeyValueTargetingPairs + bidObject.adserverTargeting = Object.assign(bidObject.adserverTargeting || {}, keyValues); + + return bidObject; +} + export function getStandardBidderSettings() { let granularity = config.getConfig('priceGranularity'); let bidder_settings = $$PREBID_GLOBAL$$.bidderSettings; diff --git a/src/hook.js b/src/hook.js new file mode 100644 index 00000000000..5ba1d4b9bbf --- /dev/null +++ b/src/hook.js @@ -0,0 +1,78 @@ + +/** + * @typedef {function} HookedFunction + * @property {function(function(), [number])} addHook A method that takes a new function to attach as a hook + * to the HookedFunction + * @property {function(function())} removeHook A method to remove attached hooks + */ + +/** + * A map of global hook methods to allow easy extension of hooked functions that are intended to be extended globally + * @type {{}} + */ +export const hooks = {}; + +/** + * A utility function for allowing a regular function to be extensible with additional hook functions + * @param {string} type The method for applying all attached hooks when this hooked function is called + * @param {function()} fn The function to make hookable + * @param {string} hookName If provided this allows you to register a name for a global hook to have easy access to + * the addHook and removeHook methods for that hook (which are usually accessed as methods on the function itself) + * @returns {HookedFunction} A new function that implements the HookedFunction interface + */ +export function createHook(type, fn, hookName) { + let _hooks = [{fn, priority: 0}]; + + let types = { + sync: function(...args) { + _hooks.forEach(hook => { + hook.fn.apply(this, args); + }); + }, + asyncSeries: function(...args) { + let curr = 0; + + const asyncSeriesNext = (...args) => { + let hook = _hooks[++curr]; + if (typeof hook === 'object' && typeof hook.fn === 'function') { + return hook.fn.apply(this, args.concat(asyncSeriesNext)) + } + }; + + return _hooks[curr].fn.apply(this, args.concat(asyncSeriesNext)); + } + }; + + if (!types[type]) { + throw 'invalid hook type'; + } + + let methods = { + addHook: function(fn, priority = 10) { + if (typeof fn === 'function') { + _hooks.push({ + fn, + priority: priority + }); + + _hooks.sort((a, b) => b.priority - a.priority); + } + }, + removeHook: function(removeFn) { + _hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); + } + }; + + if (typeof hookName === 'string') { + hooks[hookName] = methods; + } + + function hookedFn(...args) { + if (_hooks.length === 0) { + return fn.apply(this, args); + } + return types[type].apply(this, args); + } + + return Object.assign(hookedFn, methods); +} diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 755886e4d37..2f6d9fa5daa 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -561,101 +561,6 @@ describe('auctionmanager.js', function () { assert.equal(registeredBid.adserverTargeting.extra, 'stuff'); }); - it('should add native bids that do have required assets', () => { - sinon.stub(utils, 'getBidRequest', () => ({ - start: timestamp(), - bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, - }, - mediaType: 'native', - })); - - let bids1 = Object.assign({}, - bids[0], - { - mediaType: 'native', - native: { - title: 'Native Creative', - clickUrl: 'https://www.link.example', - }, - } - ); - - const bidsRecCount = auction.getBidsReceived().length; - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids1); - auction.callBids(); - assert.equal(bidsRecCount + 1, auction.getBidsReceived().length); - - utils.getBidRequest.restore(); - }); - - it('should not add native bids that do not have required assets', () => { - sinon.stub(utils, 'getBidRequest', () => ({ - start: timestamp(), - bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, - }, - mediaType: 'native', - })); - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'appnexusAst', - mediaType: 'native', - native: {title: undefined} - } - ); - - const bidsRecCount = auction.getBidsReceived().length; - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids1); - auction.callBids(); - assert.equal(bidsRecCount, auction.getBidsReceived().length); - - utils.getBidRequest.restore(); - }); - - it('requires a renderer on outstream bids', () => { - const bidRequest = () => ({ - start: timestamp(), - bidder: 'sampleBidder', - mediaTypes: { - video: {context: 'outstream'} - }, - }); - - sinon.stub(utils, 'getBidRequest', bidRequest); - sinon.stub(utils, 'getBidderRequest', bidRequest); - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'sampleBidder', - mediaType: 'video', - renderer: {render: () => true, url: 'render.js'}, - } - ); - - const bidsRecCount = auction.getBidsReceived().length; - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids1); - auction.callBids(); - assert.equal(bidsRecCount + 1, auction.getBidsReceived().length); - - utils.getBidRequest.restore(); - utils.getBidderRequest.restore(); - }); - it('installs publisher-defined renderers on bids', () => { let bidRequests = [{ 'bidderCode': BIDDER_CODE, @@ -698,52 +603,6 @@ describe('auctionmanager.js', function () { const addedBid = auction.getBidsReceived().pop(); assert.equal(addedBid.renderer.url, 'renderer.js'); }); - - it('should add banner bids that have no width or height but single adunit size', () => { - let bidRequests = [{ - 'bidderCode': BIDDER_CODE, - 'auctionId': '20882439e3238c', - 'bidderRequestId': '331f3cf3f1d9c8', - 'bids': [ - { - 'bidder': BIDDER_CODE, - 'params': { - 'placementId': 'id' - }, - 'adUnitCode': 'adUnit-code', - 'sizes': [[300, 250]], - 'bidId': '4d0a6829338a07', - 'bidderRequestId': '331f3cf3f1d9c8', - 'auctionId': '20882439e3238c', - 'renderer': { - url: 'renderer.js', - render: (bid) => bid - } - } - ], - 'auctionStart': 1505250713622, - 'timeout': 3000 - }]; - - makeRequestsStub.returns(bidRequests); - - let bids1 = Object.assign({}, - bids[0], - { - width: undefined, - height: undefined - } - ); - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids1); - auction.callBids(); - const addedBid = auction.getBidsReceived().pop(); - - assert.equal(addedBid.width, 300); - assert.equal(addedBid.height, 250); - }); }); describe('addBidResponse', () => { diff --git a/test/spec/hook_spec.js b/test/spec/hook_spec.js new file mode 100644 index 00000000000..1fab4ecd1b7 --- /dev/null +++ b/test/spec/hook_spec.js @@ -0,0 +1,151 @@ + +import { expect } from 'chai'; +import { createHook, hooks } from 'src/hook'; + +describe('the hook module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should call all sync hooks attached to a function', () => { + let called = []; + let calledWith; + + let testFn = () => { + called.push(testFn); + }; + let testHook = (...args) => { + called.push(testHook); + calledWith = args; + }; + let testHook2 = () => { + called.push(testHook2); + }; + let testHook3 = () => { + called.push(testHook3); + }; + + let hookedTestFn = createHook('sync', testFn, 'testHook'); + + hookedTestFn.addHook(testHook, 50); + hookedTestFn.addHook(testHook2, 100); + + // make sure global test hooks work as well (with default priority) + hooks['testHook'].addHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testHook, + testHook3, + testFn + ]); + + expect(calledWith).to.deep.equal([1, 2, 3]); + + called = []; + + hookedTestFn.removeHook(testHook); + hooks['testHook'].removeHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext = {}; + let calledBoundContext; + let hook = function() { + calledBoundContext = this; + }.bind(boundContext); + + let hookFn = createHook('sync', fn); + hookFn.addHook(hook); + + let newContext = {}; + hookFn.bind(newContext)(); + + expect(context).to.equal(newContext); + expect(calledBoundContext).to.equal(boundContext); + }); + + describe('asyncSeries', () => { + it('should call function as normal if no hooks attached', () => { + let fn = sandbox.spy(); + let hookFn = createHook('asyncSeries', fn); + + hookFn(1); + + expect(fn.calledOnce).to.equal(true); + expect(fn.firstCall.args[0]).to.equal(1); + }); + + it('should call hooks correctly applied in asyncSeries', () => { + let called = []; + + let testFn = (called) => { + called.push(testFn); + }; + let testHook = (called, next) => { + called.push(testHook); + next(called); + }; + let testHook2 = (called, next) => { + called.push(testHook2); + next(called); + }; + + let hookedTestFn = createHook('asyncSeries', testFn); + hookedTestFn.addHook(testHook); + hookedTestFn.addHook(testHook2); + + hookedTestFn(called); + + expect(called).to.deep.equal([ + testHook, + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext1 = {}; + let calledBoundContext1; + let hook1 = function(next) { + calledBoundContext1 = this; + next() + }.bind(boundContext1); + + let hookFn = createHook('asyncSeries', fn); + hookFn.addHook(hook1); + + let newContext = {}; + hookFn = hookFn.bind(newContext); + hookFn(); + + expect(context).to.equal(newContext); + expect(calledBoundContext1).to.equal(boundContext1); + }); + }); +}); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 937e6a084e4..37bb01b7c16 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -5,17 +5,22 @@ import { import { setConfig, - addBidResponseDecorator, - + addBidResponseHook, currencySupportEnabled, currencyRates } from 'modules/currency'; +import { createHook } from 'src/hook'; + var assert = require('chai').assert; var expect = require('chai').expect; describe('currency', function () { let fakeCurrencyFileServer; + + let fn = sinon.spy(); + let hookFn = createHook('asyncSeries', fn, 'addBidResponse'); + beforeEach(() => { fakeCurrencyFileServer = sinon.fakeServer.create(); }); @@ -46,10 +51,6 @@ describe('currency', function () { var bid = { cpm: 1, bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'GBP', bidderCurrencyDefault: { @@ -57,7 +58,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('GBP') }); @@ -68,10 +71,6 @@ describe('currency', function () { var bid = { cpm: 1, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'JPY', bidderCurrencyDefault: { @@ -79,7 +78,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('JPY') }); @@ -97,12 +98,10 @@ describe('currency', function () { var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); - expect(innerBid.cpm).to.equal('1.0000'); }); }); @@ -113,14 +112,15 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var marker = false; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - marker = true; - }); var bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - wrappedAddBidResponseFn('elementId', bid); + + var marker = false; + addBidResponseHook('elementId', bid, function() { + marker = true; + }); + expect(marker).to.equal(false); fakeCurrencyFileServer.respond(); @@ -133,10 +133,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); }); @@ -144,10 +143,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -157,10 +155,9 @@ describe('currency', function () { }); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(bid).to.equal(innerBid); }); @@ -170,10 +167,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'ABC' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -183,10 +179,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -196,10 +191,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); expect(innerBid.currency).to.equal('JPY'); }); @@ -210,10 +204,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.7798'); expect(innerBid.currency).to.equal('GBP'); }); @@ -224,10 +217,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'CNY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.1133'); expect(innerBid.currency).to.equal('GBP'); }); @@ -238,10 +230,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.0623'); expect(innerBid.currency).to.equal('CNY'); }); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 41b84449649..7358017474d 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -405,7 +405,7 @@ describe('bidders created by newBidder', () => { const bidder = newBidder(spec); const bid = { - requestId: 'some-id', + requestId: '1', ad: 'ad-url.com', cpm: 0.5, height: 200, @@ -554,3 +554,193 @@ describe('registerBidder', () => { expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); }) + +describe('validate bid response: ', () => { + let spec; + let bidder; + let addBidResponseStub; + let doneStub; + let ajaxStub; + let logErrorSpy; + + let bids = [{ + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }]; + + beforeEach(() => { + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + }; + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + + addBidResponseStub = sinon.stub(); + doneStub = sinon.stub(); + ajaxStub = sinon.stub(ajax, 'ajax', function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(() => { + ajaxStub.restore(); + logErrorSpy.restore(); + }); + + it('should add native bids that do have required assets', () => { + let bidRequest = { + bids: [{ + bidId: 1, + requestId: 'first-bid-id', + adUnitCode: 'mock/placement', + params: { + param: 5 + }, + nativeParams: { + title: {'required': true}, + }, + mediaType: 'native', + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + 'mediaType': 'native', + 'native': { + 'title': 'Native Creative', + 'clickUrl': 'https://www.link.example', + } + } + ); + + const bidder = newBidder(spec); + + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); + + it('should not add native bids that do not have required assets', () => { + let bidRequest = { + bids: [{ + bidId: 1, + requestId: 'first-bid-id', + adUnitCode: 'mock/placement', + params: { + param: 5 + }, + nativeParams: { + title: {'required': true}, + }, + mediaType: 'native', + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'native', + native: { + title: undefined, + clickUrl: 'https://www.link.example', + } + } + ); + + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub); + + expect(addBidResponseStub.calledOnce).to.equal(false); + expect(logErrorSpy.callCount).to.equal(1); + }); + + it('should add bid when renderer is present on outstream bids', () => { + let bidRequest = { + bids: [{ + bidId: 1, + requestId: 'first-bid-id', + adUnitCode: 'mock/placement', + params: { + param: 5 + }, + mediaTypes: { + video: {context: 'outstream'} + } + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'video', + renderer: {render: () => true, url: 'render.js'}, + } + ); + + const bidder = newBidder(spec); + + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); + + it('should add banner bids that have no width or height but single adunit size', () => { + let bidRequest = { + bids: [{ + bidder: CODE, + bidId: 1, + requestId: 'first-bid-id', + adUnitCode: 'mock/placement', + params: { + param: 5 + }, + sizes: [[300, 250]], + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + width: undefined, + height: undefined + } + ); + + const bidder = newBidder(spec); + + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); +});