diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js new file mode 100644 index 00000000000..00c78e2d443 --- /dev/null +++ b/modules/prebidServerBidAdapter.js @@ -0,0 +1,318 @@ +import Adapter from 'src/adapter'; +import bidfactory from 'src/bidfactory'; +import * as utils from 'src/utils'; +import { ajax } from 'src/ajax'; +import { STATUS, S2S } from 'src/constants'; +import { cookieSet } from 'src/cookie.js'; +import adaptermanager from 'src/adaptermanager'; +import { config } from 'src/config'; +import { VIDEO } from 'src/mediaTypes'; + +const getConfig = config.getConfig; + +const TYPE = S2S.SRC; +let _synced = false; + +let _s2sConfig; +config.setDefaults({ + 's2sConfig': { + enabled: false, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer' + } +}); + +/** + * Set config for server to server header bidding + * @typedef {Object} options - required + * @property {boolean} enabled enables S2S bidding + * @property {string[]} bidders bidders to request S2S + * @property {string} endpoint endpoint to contact + * === optional params below === + * @property {number} [timeout] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` + * @property {string} [adapter] adapter code to use for S2S + * @property {string} [syncEndpoint] endpoint URL for syncing cookies + * @property {string} [cookieSetUrl] url for cookie set library, if passed then cookieSet is enabled + */ +function setS2sConfig(options) { + let keys = Object.keys(options); + + if (['accountId', 'bidders', 'endpoint'].filter(key => { + if (!keys.includes(key)) { + utils.logError(key + ' missing in server to server config'); + return true; + } + return false; + }).length > 0) { + return; + } + + _s2sConfig = options; + if (options.syncEndpoint) { + queueSync(options.bidders); + } +} +getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); + +/** + * @param {Array} bidderCodes list of bidders to request user syncs for. + */ +function queueSync(bidderCodes) { + if (_synced) { + return; + } + _synced = true; + const payload = JSON.stringify({ + uuid: utils.generateUUID(), + bidders: bidderCodes + }); + ajax(_s2sConfig.syncEndpoint, (response) => { + try { + response = JSON.parse(response); + response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); + } catch (e) { + utils.logError(e); + } + }, + payload, { + contentType: 'text/plain', + withCredentials: true + }); +} + +/** + * Run a cookie sync for the given type, url, and bidder + * + * @param {string} type the type of sync, "image", "redirect", "iframe" + * @param {string} url the url to sync + * @param {string} bidder name of bidder doing sync for + */ +function doBidderSync(type, url, bidder) { + if (!url) { + utils.logError(`No sync url for bidder "${bidder}": ${url}`); + } else if (type === 'image' || type === 'redirect') { + utils.logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`); + utils.triggerPixel(url); + } else if (type == 'iframe') { + utils.logMessage(`Invoking iframe user sync for bidder: "${bidder}"`); + utils.insertUserSyncIframe(url); + } else { + utils.logError(`User sync type "${type}" not supported for bidder: "${bidder}"`); + } +} + +/** + * Try to convert a value to a type. + * If it can't be done, the value will be returned. + * + * @param {string} typeToConvert The target type. e.g. "string", "number", etc. + * @param {*} value The value to be converted into typeToConvert. + */ +function tryConvertType(typeToConvert, value) { + if (typeToConvert === 'string') { + return value && value.toString(); + } else if (typeToConvert === 'number') { + return Number(value); + } else { + return value; + } +} + +const tryConvertString = tryConvertType.bind(null, 'string'); +const tryConvertNumber = tryConvertType.bind(null, 'number'); + +const paramTypes = { + 'appnexus': { + 'member': tryConvertString, + 'invCode': tryConvertString, + 'placementId': tryConvertNumber + }, + 'rubicon': { + 'accountId': tryConvertNumber, + 'siteId': tryConvertNumber, + 'zoneId': tryConvertNumber + }, + 'indexExchange': { + 'siteID': tryConvertNumber + }, + 'audienceNetwork': { + 'placementId': tryConvertString + }, + 'pubmatic': { + 'publisherId': tryConvertString, + 'adSlot': tryConvertString + }, + 'districtm': { + 'member': tryConvertString, + 'invCode': tryConvertString, + 'placementId': tryConvertNumber + }, + 'pulsepoint': { + 'cf': tryConvertString, + 'cp': tryConvertNumber, + 'ct': tryConvertNumber + }, +}; + +/** + * Bidder adapter for Prebid Server + */ +export function PrebidServer() { + let baseAdapter = new Adapter('prebidServer'); + + function convertTypes(adUnits) { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const types = paramTypes[bid.bidder] || []; + Object.keys(types).forEach(key => { + if (bid.params[key]) { + bid.params[key] = types[key](bid.params[key]); + + // don't send invalid values + if (isNaN(bid.params[key])) { + delete bid.params.key; + } + } + }); + }); + }); + } + + /* Prebid executes this function when the page asks to send out bid requests */ + baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { + const isDebug = !!getConfig('debug'); + const adUnits = utils.cloneJson(s2sBidRequest.ad_units); + adUnits.forEach(adUnit => { + let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video'); + if (videoMediaType) { + // pbs expects a ad_unit.video attribute if the imp is video + adUnit.video = Object.assign({}, videoMediaType); + delete adUnit.mediaTypes; + // default is assumed to be 'banner' so if there is a video type we assume video only until PBS can support multi format auction. + adUnit.media_types = [VIDEO]; + } + }); + convertTypes(adUnits); + let requestJson = { + account_id: _s2sConfig.accountId, + tid: s2sBidRequest.tid, + max_bids: _s2sConfig.maxBids, + timeout_millis: _s2sConfig.timeout, + secure: _s2sConfig.secure, + url: utils.getTopWindowUrl(), + prebid_version: '$prebid.version$', + ad_units: adUnits.filter(hasSizes), + is_debug: isDebug + }; + + // in case config.bidders contains invalid bidders, we only process those we sent requests for. + const requestedBidders = requestJson.ad_units.map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(utils.uniques)).reduce(utils.flatten).filter(utils.uniques); + function processResponse(response) { + handleResponse(response, requestedBidders, bidRequests, addBidResponse, done); + } + const payload = JSON.stringify(requestJson); + ajax(_s2sConfig.endpoint, processResponse, payload, { + contentType: 'text/plain', + withCredentials: true + }); + }; + + // at this point ad units should have a size array either directly or mapped so filter for that + function hasSizes(unit) { + return unit.sizes && unit.sizes.length; + } + + /* Notify Prebid of bid responses so bids can get in the auction */ + function handleResponse(response, requestedBidders, bidRequests, addBidResponse, done) { + let result; + try { + result = JSON.parse(response); + + if (result.status === 'OK' || result.status === 'no_cookie') { + if (result.bidder_status) { + result.bidder_status.forEach(bidder => { + if (bidder.no_cookie) { + doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); + } + }); + } + + // do client-side syncs if available + requestedBidders.forEach(bidder => { + let clientAdapter = adaptermanager.getBidAdapter(bidder); + if (clientAdapter && clientAdapter.registerSyncs) { + clientAdapter.registerSyncs(); + } + }); + + if (result.bids) { + result.bids.forEach(bidObj => { + let bidRequest = utils.getBidRequest(bidObj.bid_id, bidRequests); + let cpm = bidObj.price; + let status; + if (cpm !== 0) { + status = STATUS.GOOD; + } else { + status = STATUS.NO_BID; + } + + let bidObject = bidfactory.createBid(status, bidRequest); + bidObject.source = TYPE; + bidObject.creative_id = bidObj.creative_id; + bidObject.bidderCode = bidObj.bidder; + bidObject.cpm = cpm; + // From ORTB see section 4.2.3: adm Optional means of conveying ad markup in case the bid wins; supersedes the win notice if markup is included in both. + if (bidObj.media_type === VIDEO) { + bidObject.mediaType = VIDEO; + if (bidObj.adm) { + bidObject.vastXml = bidObj.adm; + } + if (bidObj.nurl) { + bidObject.vastUrl = bidObj.nurl; + } + } else { + if (bidObj.adm && bidObj.nurl) { + bidObject.ad = bidObj.adm; + bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl)); + } else if (bidObj.adm) { + bidObject.ad = bidObj.adm; + } else if (bidObj.nurl) { + bidObject.adUrl = bidObj.nurl + } + } + + bidObject.width = bidObj.width; + bidObject.height = bidObj.height; + bidObject.adserverTargeting = bidObj.ad_server_targeting; + if (bidObj.deal_id) { + bidObject.dealId = bidObj.deal_id; + } + + addBidResponse(bidObj.code, bidObject); + }); + } + } + if (result.status === 'no_cookie' && typeof _s2sConfig.cookieSetUrl === 'string') { + // cookie sync + cookieSet(_s2sConfig.cookieSetUrl); + } + } catch (error) { + utils.logError(error); + } + + if (!result || (result.status && result.status.includes('Error'))) { + utils.logError('error parsing response: ', result.status); + } + + done(); + } + + return Object.assign(this, { + callBids: baseAdapter.callBids, + setBidderCode: baseAdapter.setBidderCode, + type: TYPE + }); +} + +adaptermanager.registerBidAdapter(new PrebidServer(), 'prebidServer'); diff --git a/src/adaptermanager.js b/src/adaptermanager.js index c6568c0c9f7..a009e91cd24 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -15,7 +15,10 @@ let s2sTestingModule; // store s2sTesting module if it's loaded var _bidderRegistry = {}; exports.bidderRegistry = _bidderRegistry; -let _s2sConfig = config.getConfig('s2sConfig'); +let _s2sConfig = {}; +config.getConfig('s2sConfig', config => { + _s2sConfig = config.s2sConfig; +}); var _analyticsRegistry = {}; @@ -89,19 +92,30 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels}) { }, []).reduce(flatten, []).filter(val => val !== ''); } +function transformHeightWidth(adUnit) { + let sizesObj = []; + let sizes = utils.parseSizesInput(adUnit.sizes); + sizes.forEach(size => { + let heightWidth = size.split('x'); + let sizeObj = { + 'w': parseInt(heightWidth[0]), + 'h': parseInt(heightWidth[1]) + }; + sizesObj.push(sizeObj); + }); + return sizesObj; +} + function getAdUnitCopyForPrebidServer(adUnits) { let adaptersServerSide = _s2sConfig.bidders; let adUnitsCopy = utils.cloneJson(adUnits); - // filter out client side bids adUnitsCopy.forEach((adUnit) => { - if (adUnit.sizeMapping) { - adUnit.sizes = mapSizes(adUnit); - delete adUnit.sizeMapping; - } adUnit.sizes = transformHeightWidth(adUnit); + + // filter out client side bids adUnit.bids = adUnit.bids.filter((bid) => { - return adaptersServerSide.includes(bid.bidder) && (!s2sTesting || bid.finalSource !== s2sTestingModule.CLIENT); + return adaptersServerSide.includes(bid.bidder) && (!doingS2STesting() || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; @@ -115,6 +129,23 @@ function getAdUnitCopyForPrebidServer(adUnits) { return adUnitsCopy; } +function getAdUnitCopyForClientAdapters(adUnits) { + let adUnitsClientCopy = utils.cloneJson(adUnits); + // filter out s2s bids + adUnitsClientCopy.forEach((adUnit) => { + adUnit.bids = adUnit.bids.filter((bid) => { + return !doingS2STesting() || bid.finalSource !== s2sTestingModule.SERVER; + }) + }); + + // don't send empty requests + adUnitsClientCopy = adUnitsClientCopy.filter(adUnit => { + return adUnit.bids.length !== 0; + }); + + return adUnitsClientCopy; +} + exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) { let bidRequests = []; let bidderCodes = getBidderCodes(adUnits); @@ -122,18 +153,11 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, bidderCodes = shuffle(bidderCodes); } - const s2sAdapter = _bidderRegistry[_s2sConfig.adapter]; - if (s2sAdapter) { - s2sAdapter.setConfig(_s2sConfig); - s2sAdapter.queueSync({bidderCodes}); - } - + let clientBidderCodes = bidderCodes; let clientTestAdapters = []; - let s2sTesting = false; if (_s2sConfig.enabled) { // if s2sConfig.bidderControl testing is turned on - s2sTesting = _s2sConfig.testing && typeof s2sTestingModule !== 'undefined'; - if (s2sTesting) { + if (doingS2STesting()) { // get all adapters doing client testing clientTestAdapters = s2sTestingModule.getSourceBidderMap(adUnits)[s2sTestingModule.CLIENT]; } @@ -142,11 +166,11 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, let adaptersServerSide = _s2sConfig.bidders; // don't call these client side (unless client request is needed for testing) - bidderCodes = bidderCodes.filter((elm) => { + clientBidderCodes = bidderCodes.filter((elm) => { return !adaptersServerSide.includes(elm) || clientTestAdapters.includes(elm); }); - let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits); + let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits); let tid = utils.generateUUID(); adaptersServerSide.forEach(bidderCode => { const bidderRequestId = utils.getUniqueIdentifierStr(); @@ -166,27 +190,15 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, }); } - // client side adapters - let adUnitsClientCopy = utils.cloneJson(adUnits); - // filter out s2s bids - adUnitsClientCopy.forEach((adUnit) => { - adUnit.bids = adUnit.bids.filter((bid) => { - return !s2sTesting || bid.finalSource !== s2sTestingModule.SERVER; - }) - }); - - // don't send empty requests - adUnitsClientCopy = adUnitsClientCopy.filter(adUnit => { - return adUnit.bids.length !== 0; - }); - - bidderCodes.forEach(bidderCode => { + // client adapters + let adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits); + clientBidderCodes.forEach(bidderCode => { const bidderRequestId = utils.getUniqueIdentifierStr(); const bidderRequest = { bidderCode, auctionId, bidderRequestId, - bids: getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels}), + bids: getBids({bidderCode, auctionId, bidderRequestId, 'adUnits': adUnitsClientCopy, labels}), auctionStart: auctionStart, timeout: cbTimeout }; @@ -198,9 +210,17 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, }; exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { - let serverBidRequests = bidRequests.filter(bidRequest => { - return bidRequest.src && bidRequest.src === CONSTANTS.S2S.SRC; - }); + if (!bidRequests.length) { + utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); + return; + } + + let ajax = ajaxBuilder(bidRequests[0].timeout); + + let [clientBidRequests, serverBidRequests] = bidRequests.reduce((partitions, bidRequest) => { + partitions[Number(typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC)].push(bidRequest); + return partitions; + }, [[], []]); if (serverBidRequests.length) { let adaptersServerSide = _s2sConfig.bidders; @@ -209,45 +229,56 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { if (s2sAdapter) { let s2sBidRequest = {tid, 'ad_units': getAdUnitCopyForPrebidServer(adUnits)}; - utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.join(',')}`); if (s2sBidRequest.ad_units.length) { - s2sAdapter.callBids(s2sBidRequest); + let doneCbs = serverBidRequests.map(bidRequest => { + bidRequest.doneCbCallCount = 0; + return doneCb(bidRequest.bidderRequestId) + }); + + // only log adapters that actually have adUnit bids + let allBidders = s2sBidRequest.ad_units.reduce((adapters, adUnit) => { + return adapters.concat((adUnit.bids || []).reduce((adapters, bid) => { return adapters.concat(bid.bidderCode) }, [])); + }, []); + utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => { + return allBidders.includes(adapter); + }).join(',')}`); + + // fire BID_REQUESTED event for each s2s bidRequest + serverBidRequests.forEach(bidRequest => { + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + }); + + // make bid requests + s2sAdapter.callBids( + s2sBidRequest, + serverBidRequests, + addBidResponse, + () => doneCbs.forEach(done => done()), + ajax + ); } } } - if (bidRequests.length) { - let ajax = ajaxBuilder(bidRequests[0].timeout); - bidRequests.forEach(bidRequest => { - bidRequest.start = new Date().getTime(); - // TODO : Do we check for bid in pool from here and skip calling adapter again ? - const adapter = _bidderRegistry[bidRequest.bidderCode]; - if (adapter) { - utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); - bidRequest.doneCbCallCount = 0; - let done = doneCb(bidRequest.bidderRequestId); - adapter.callBids(bidRequest, addBidResponse, done, ajax); - } else { - utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); - } - }); - } else { - utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); - } -}; -function transformHeightWidth(adUnit) { - let sizesObj = []; - let sizes = utils.parseSizesInput(adUnit.sizes); - sizes.forEach(size => { - let heightWidth = size.split('x'); - let sizeObj = { - 'w': parseInt(heightWidth[0]), - 'h': parseInt(heightWidth[1]) - }; - sizesObj.push(sizeObj); + // handle client adapter requests + clientBidRequests.forEach(bidRequest => { + bidRequest.start = new Date().getTime(); + // TODO : Do we check for bid in pool from here and skip calling adapter again ? + const adapter = _bidderRegistry[bidRequest.bidderCode]; + if (adapter) { + utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + bidRequest.doneCbCallCount = 0; + let done = doneCb(bidRequest.bidderRequestId); + adapter.callBids(bidRequest, addBidResponse, done, ajax); + } else { + utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); + } }); - return sizesObj; +} + +function doingS2STesting() { + return _s2sConfig && _s2sConfig.enabled && _s2sConfig.testing && s2sTestingModule; } function getSupportedMediaTypes(bidderCode) { @@ -340,6 +371,10 @@ exports.enableAnalytics = function (config) { }); }; +exports.getBidAdapter = function(bidder) { + return _bidderRegistry[bidder]; +}; + // the s2sTesting module is injected when it's loaded rather than being imported // importing it causes the packager to include it even when it's not explicitly included in the build exports.setS2STestingModule = function (module) { diff --git a/src/config.js b/src/config.js index bbde171576c..9ff57777984 100644 --- a/src/config.js +++ b/src/config.js @@ -22,16 +22,6 @@ const DEFAULT_USERSYNC = { syncDelay: 3000 }; const DEFAULT_TIMEOUTBUFFER = 200; -const DEFAULT_S2SCONFIG = { - enabled: false, - endpoint: 'https://prebid.adnxs.com/pbs/v1/auction', - timeout: 1000, - maxBids: 1, - adapter: 'prebidServer', - syncEndpoint: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - cookieSet: true, - bidders: [] -}; export const RANDOM = 'random'; const FIXED = 'fixed'; @@ -64,6 +54,8 @@ const ALL_TOPICS = '*'; export function newConfig() { let listeners = []; + let defaults = {}; + let config = { // `debug` is equivalent to legacy `pbjs.logging` property _debug: DEFAULT_DEBUG, @@ -152,24 +144,6 @@ export function newConfig() { this._timoutBuffer = val; }, - _s2sConfig: DEFAULT_S2SCONFIG, - get s2sConfig() { - return this._s2sConfig; - }, - set s2sConfig(val) { - if (!utils.contains(Object.keys(val), 'accountId')) { - utils.logError('accountId missing in Server to Server config'); - return; - } - - if (!utils.contains(Object.keys(val), 'bidders')) { - utils.logError('bidders missing in Server to Server config'); - return; - } - - this._s2sConfig = Object.assign({}, DEFAULT_S2SCONFIG, val); - }, - // userSync defaults userSync: DEFAULT_USERSYNC }; @@ -223,8 +197,33 @@ export function newConfig() { return; } - Object.assign(config, options); - callSubscribers(options); + let topics = Object.keys(options); + let topicalConfig = {}; + + topics.forEach(topic => { + let option = options[topic]; + + if (typeof defaults[topic] === 'object' && typeof option === 'object') { + option = Object.assign({}, defaults[topic], option); + } + + topicalConfig[topic] = config[topic] = option; + }); + + callSubscribers(topicalConfig); + } + + /** + * Sets configuration defaults which setConfig values can be applied on top of + * @param {object} options + */ + function setDefaults(options) { + if (typeof defaults !== 'object') { + utils.logError('defaults must be an object'); + return; + } + + Object.assign(defaults, options); } /* @@ -292,7 +291,8 @@ export function newConfig() { return { getConfig, - setConfig + setConfig, + setDefaults }; } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js new file mode 100644 index 00000000000..608ac102ace --- /dev/null +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -0,0 +1,479 @@ +import { expect } from 'chai'; +import { PrebidServer as Adapter } from 'modules/prebidServerBidAdapter'; +import adapterManager from 'src/adaptermanager'; +import * as utils from 'src/utils'; +import cookie from 'src/cookie'; +import { userSync } from 'src/userSync'; +import { ajax } from 'src/ajax'; +import { config } from 'src/config'; + +let CONFIG = { + accountId: '1', + enabled: true, + bidders: ['appnexus'], + timeout: 1000, + endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' +}; + +const REQUEST = { + 'account_id': '1', + 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'max_bids': 1, + 'timeout_millis': 1000, + 'secure': 0, + 'url': '', + 'prebid_version': '0.30.0-pre', + 'ad_units': [ + { + 'code': 'div-gpt-ad-1460505748561-0', + 'sizes': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 300, + 'h': 600 + } + ], + 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', + 'bids': [ + { + 'bid_id': '123', + 'bidder': 'appnexus', + 'params': { + 'placementId': '10433394', + 'member': 123 + } + } + ] + } + ] +}; + +const BID_REQUESTS = [ + { + 'bidderCode': 'appnexus', + 'auctionId': '173afb6d132ba3', + 'bidderRequestId': '3d1063078dfcc8', + 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '10433394', + 'member': 123 + }, + 'bid_id': '123', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', + 'sizes': [ + { + 'w': 300, + 'h': 250 + } + ], + 'bidId': '259fb43aaa06c1', + 'bidderRequestId': '3d1063078dfcc8', + 'auctionId': '173afb6d132ba3' + } + ], + 'auctionStart': 1510852447530, + 'timeout': 5000, + 'src': 's2s', + 'doneCbCallCount': 0 + } +]; + +const RESPONSE = { + 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'status': 'OK', + 'bidder_status': [ + { + 'bidder': 'appnexus', + 'response_time_ms': 52, + 'num_bids': 1 + } + ], + 'bids': [ + { + 'bid_id': '123', + 'code': 'div-gpt-ad-1460505748561-0', + 'creative_id': '29681110', + 'bidder': 'appnexus', + 'price': 0.5, + 'adm': '', + 'width': 300, + 'height': 250, + 'deal_id': 'test-dealid', + 'ad_server_targeting': { + 'foo': 'bar' + } + } + ] +}; + +const RESPONSE_NO_BID_NO_UNIT = { + 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'status': 'OK', + 'bidder_status': [{ + 'bidder': 'appnexus', + 'response_time_ms': 132, + 'no_bid': true + }] +}; + +const RESPONSE_NO_BID_UNIT_SET = { + 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'status': 'OK', + 'bidder_status': [{ + 'bidder': 'appnexus', + 'ad_unit': 'div-gpt-ad-1460505748561-0', + 'response_time_ms': 91, + 'no_bid': true + }] +}; + +const RESPONSE_NO_COOKIE = { + 'tid': 'd6eca075-4a59-4346-bdb3-86531830ef2c', + 'status': 'OK', + 'bidder_status': [{ + 'bidder': 'pubmatic', + 'no_cookie': true, + 'usersync': { + 'url': '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=http://localhost:8000/setuid?bidder=pubmatic&uid=', + 'type': 'iframe' + } + }] +}; + +const RESPONSE_NO_PBS_COOKIE = { + 'tid': '882fe33e-2981-4257-bd44-bd3b03945f48', + 'status': 'no_cookie', + 'bidder_status': [{ + 'bidder': 'rubicon', + 'no_cookie': true, + 'usersync': { + 'url': 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid', + 'type': 'redirect' + } + }, { + 'bidder': 'pubmatic', + 'no_cookie': true, + 'usersync': { + 'url': '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D', + 'type': 'iframe' + } + }, { + 'bidder': 'appnexus', + 'response_time_ms': 162, + 'num_bids': 1, + 'debug': [{ + 'request_uri': 'http://ib.adnxs.com/openrtb2', + 'request_body': '{"id":"882fe33e-2981-4257-bd44-bd3b03945f48","imp":[{"id":"/19968336/header-bid-tag-0","banner":{"w":300,"h":250,"format":[{"w":300,"h":250}]},"secure":1,"ext":{"appnexus":{"placement_id":5914989}}}],"site":{"domain":"nytimes.com","page":"http://www.nytimes.com"},"device":{"ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36","ip":"75.97.0.47"},"user":{"id":"3519479852893340159","buyeruid":"3519479852893340159"},"at":1,"tmax":1000,"source":{"fd":1,"tid":"882fe33e-2981-4257-bd44-bd3b03945f48"}}', + 'response_body': '{"id":"882fe33e-2981-4257-bd44-bd3b03945f48"}', + 'status_code': 200 + }] + }], + 'bids': [{ + 'bid_id': '123', + 'code': 'div-gpt-ad-1460505748561-0', + 'creative_id': '70928274', + 'bidder': 'appnexus', + 'price': 0.07425, + 'adm': '', + 'width': 300, + 'height': 250, + 'response_time_ms': 162 + }] +}; + +const RESPONSE_NO_PBS_COOKIE_ERROR = { + 'tid': '882fe33e-2981-4257-bd44-bd3b0394545f', + 'status': 'no_cookie', + 'bidder_status': [{ + 'bidder': 'rubicon', + 'no_cookie': true, + 'usersync': { + 'url': 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid', + 'type': 'jsonp' + } + }, { + 'bidder': 'pubmatic', + 'no_cookie': true, + 'usersync': { + 'url': '', + 'type': 'iframe' + } + }] +}; + +describe('S2S Adapter', () => { + let adapter, + addBidResponse = sinon.spy(), + done = sinon.spy(); + + beforeEach(() => adapter = new Adapter()); + + afterEach(() => { + addBidResponse.reset(); + done.reset(); + }); + + describe('request function', () => { + let xhr; + let requests; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(() => xhr.restore()); + + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + it('exists converts types', () => { + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(requests[0].requestBody); + expect(requestBid.ad_units[0].bids[0].params.placementId).to.exist.and.to.be.a('number'); + expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string'); + }); + }); + + describe('response handler', () => { + let server; + + beforeEach(() => { + server = sinon.fakeServer.create(); + sinon.stub(utils, 'triggerPixel'); + sinon.stub(utils, 'insertUserSyncIframe'); + sinon.stub(utils, 'logError'); + sinon.stub(cookie, 'cookieSet'); + sinon.stub(utils, 'getBidRequest').returns({ + bidId: '123' + }); + }); + + afterEach(() => { + server.restore(); + utils.getBidRequest.restore(); + utils.triggerPixel.restore(); + utils.insertUserSyncIframe.restore(); + utils.logError.restore(); + cookie.cookieSet.restore(); + }); + + // TODO: test dependent on pbjs_api_spec. Needs to be isolated + it('registers bids', () => { + server.respondWith(JSON.stringify(RESPONSE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + sinon.assert.calledOnce(addBidResponse); + + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('cpm', 0.5); + expect(response).to.have.property('adId', '123'); + }); + + it('does not call addBidResponse and calls done when ad unit not set', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_BID_NO_UNIT)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledOnce(done); + }); + + it('does not call addBidResponse and calls done when server requests cookie sync', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_COOKIE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledOnce(done); + }); + + it('does not call addBidResponse and calls done when ad unit is set', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_BID_UNIT_SET)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledOnce(done); + }); + + it('registers successful bids and calls done when there are less bids than requests', () => { + server.respondWith(JSON.stringify(RESPONSE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.calledOnce(addBidResponse); + sinon.assert.calledOnce(done); + + expect(addBidResponse.firstCall.args[0]).to.equal('div-gpt-ad-1460505748561-0'); + + expect(addBidResponse.firstCall.args[1]).to.have.property('adId', '123'); + + expect(addBidResponse.firstCall.args[1]) + .to.have.property('statusMessage', 'Bid available'); + }); + + it('should have dealId in bidObject', () => { + server.respondWith(JSON.stringify(RESPONSE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('dealId', 'test-dealid'); + }); + + it('should pass through default adserverTargeting if present in bidObject', () => { + server.respondWith(JSON.stringify(RESPONSE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('adserverTargeting').that.deep.equals({'foo': 'bar'}); + }); + + it('registers client user syncs when client bid adapter is present', () => { + let rubiconAdapter = { + registerSyncs: sinon.spy() + }; + sinon.stub(adapterManager, 'getBidAdapter', () => rubiconAdapter); + + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.calledOnce(rubiconAdapter.registerSyncs); + + adapterManager.getBidAdapter.restore(); + }); + + it('registers bid responses when server requests cookie sync', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + sinon.assert.calledOnce(addBidResponse); + + const ad_unit_code = addBidResponse.firstCall.args[0]; + expect(ad_unit_code).to.equal('div-gpt-ad-1460505748561-0'); + + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('source', 's2s'); + + const bid_request_passed = addBidResponse.firstCall.args[1]; + expect(bid_request_passed).to.have.property('adId', '123'); + }); + + it('does cookie sync when no_cookie response', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.calledOnce(utils.triggerPixel); + sinon.assert.calledWith(utils.triggerPixel, 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid'); + sinon.assert.calledOnce(utils.insertUserSyncIframe); + sinon.assert.calledWith(utils.insertUserSyncIframe, '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D'); + }); + + it('logs error when no_cookie response is missing type or url', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR)); + + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.notCalled(utils.triggerPixel); + sinon.assert.notCalled(utils.insertUserSyncIframe); + sinon.assert.calledTwice(utils.logError); + }); + + it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + let myConfig = Object.assign({}, CONFIG); + + config.setConfig({s2sConfig: myConfig}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + sinon.assert.notCalled(cookie.cookieSet); + }); + + it('calls cookieSet cookie sync when no_cookie response && opted in', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + let myConfig = Object.assign({ + cookieSetUrl: 'https://acdn.adnxs.com/cookieset/cs.js' + }, CONFIG); + + config.setConfig({s2sConfig: myConfig}); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + sinon.assert.calledOnce(cookie.cookieSet); + }); + }); + + describe('s2sConfig', () => { + let logErrorSpy; + + beforeEach(() => { + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(() => { + utils.logError.restore(); + }); + + it('should log error when accountId is missing', () => { + const options = { + enabled: true, + bidders: ['appnexus'], + timeout: 1000, + adapter: 'prebidServer', + endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.calledOnce(logErrorSpy); + }); + + it('should log error when bidders is missing', () => { + const options = { + accountId: '1', + enabled: true, + timeout: 1000, + adapter: 's2s', + endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.calledOnce(logErrorSpy); + }); + }); +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index ae09d8598bb..5f21c5355d8 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -7,6 +7,7 @@ import { config } from 'src/config'; import { registerBidder } from 'src/adapters/bidderFactory'; import { setSizeConfig } from 'src/sizeMapping'; var s2sTesting = require('../../../../modules/s2sTesting'); +var events = require('../../../../src/events'); const CONFIG = { enabled: true, @@ -18,21 +19,15 @@ const CONFIG = { }; var prebidServerAdapterMock = { bidder: 'prebidServer', - callBids: sinon.stub(), - setConfig: sinon.stub(), - queueSync: sinon.stub() + callBids: sinon.stub() }; var adequantAdapterMock = { bidder: 'adequant', - callBids: sinon.stub(), - setConfig: sinon.stub(), - queueSync: sinon.stub() + callBids: sinon.stub() }; var appnexusAdapterMock = { bidder: 'appnexus', - callBids: sinon.stub(), - setConfig: sinon.stub(), - queueSync: sinon.stub() + callBids: sinon.stub() }; describe('adapterManager tests', () => { @@ -96,6 +91,22 @@ describe('adapterManager tests', () => { sinon.assert.called(utils.logError); }); + + it('should emit BID_REQUESTED event', () => { + // function to count BID_REQUESTED events + let cnt = 0; + let count = () => cnt++; + events.on(CONSTANTS.EVENTS.BID_REQUESTED, count); + AdapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + let adUnits = getAdUnits(); + let bidRequests = AdapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + AdapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + expect(cnt).to.equal(1); + sinon.assert.calledOnce(appnexusAdapterMock.callBids); + appnexusAdapterMock.callBids.reset(); + delete AdapterManager.bidderRegistry['appnexus']; + events.off(CONSTANTS.EVENTS.BID_REQUESTED, count); + }); }); describe('S2S tests', () => { @@ -106,8 +117,7 @@ describe('adapterManager tests', () => { prebidServerAdapterMock.callBids.reset(); }); - // Enable this test when prebidServer adapter is made 1.0 compliant - it.skip('invokes callBids on the S2S adapter', () => { + it('invokes callBids on the S2S adapter', () => { let bidRequests = [{ 'bidderCode': 'appnexus', 'requestId': '1863e370099523', @@ -166,12 +176,17 @@ describe('adapterManager tests', () => { 'start': 1462918897460 }]; - AdapterManager.callBids(getAdUnits(), bidRequests); + AdapterManager.callBids( + getAdUnits(), + bidRequests, + () => {}, + () => () => {} + ); sinon.assert.calledOnce(prebidServerAdapterMock.callBids); }); // Enable this test when prebidServer adapter is made 1.0 compliant - it.skip('invokes callBids with only s2s bids', () => { + it('invokes callBids with only s2s bids', () => { const adUnits = getAdUnits(); // adUnit without appnexus bidder adUnits.push({ @@ -245,13 +260,237 @@ describe('adapterManager tests', () => { ], 'start': 1462918897460 }]; - AdapterManager.callBids(adUnits, bidRequests); + AdapterManager.callBids( + adUnits, + bidRequests, + () => {}, + () => () => {} + ); const requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; expect(requestObj.ad_units.length).to.equal(2); sinon.assert.calledOnce(prebidServerAdapterMock.callBids); }); + + describe('BID_REQUESTED event', () => { + // function to count BID_REQUESTED events + let cnt, count = () => cnt++; + + beforeEach(() => { + cnt = 0; + events.on(CONSTANTS.EVENTS.BID_REQUESTED, count); + }); + + afterEach(() => { + events.off(CONSTANTS.EVENTS.BID_REQUESTED, count); + }); + + it('should fire for s2s requests', () => { + let adUnits = getAdUnits(); + let bidRequests = AdapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + AdapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + expect(cnt).to.equal(1); + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + }); + + it('should fire for simultaneous s2s and client requests', () => { + AdapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + let adUnits = getAdUnits(); + let bidRequests = AdapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + AdapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + expect(cnt).to.equal(2); + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + sinon.assert.calledOnce(adequantAdapterMock.callBids); + adequantAdapterMock.callBids.reset(); + delete AdapterManager.bidderRegistry['adequant']; + }); + }); }); // end s2s tests + describe('s2sTesting', () => { + function getTestAdUnits() { + // copy adUnits + return JSON.parse(JSON.stringify(getAdUnits())); + } + + function callBids(adUnits = getTestAdUnits()) { + let bidRequests = AdapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + AdapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + } + + function checkServerCalled(numAdUnits, numBids) { + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + let requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + expect(requestObj.ad_units.length).to.equal(numAdUnits); + for (let i = 0; i < numAdUnits; i++) { + expect(requestObj.ad_units[i].bids.filter((bid) => { + return bid.bidder === 'appnexus' || bid.bidder === 'adequant'; + }).length).to.equal(numBids); + } + } + + function checkClientCalled(adapter, numBids) { + sinon.assert.calledOnce(adapter.callBids); + expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); + } + + let TESTING_CONFIG = utils.cloneJson(CONFIG); + Object.assign(TESTING_CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + let stubGetSourceBidderMap; + + beforeEach(() => { + config.setConfig({s2sConfig: TESTING_CONFIG}); + AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; + AdapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + AdapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + + stubGetSourceBidderMap = sinon.stub(s2sTesting, 'getSourceBidderMap'); + + prebidServerAdapterMock.callBids.reset(); + adequantAdapterMock.callBids.reset(); + appnexusAdapterMock.callBids.reset(); + }); + + afterEach(() => { + config.setConfig({s2sConfig: {}}); + s2sTesting.getSourceBidderMap.restore(); + }); + + it('calls server adapter if no sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: [], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapter if one client source defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus'], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapters if client sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('calls client adapters if client sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call server adapter for bidders that go to client', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[1].finalSource = s2sTesting.CLIENT; + callBids(adUnits); + + // server adapter + sinon.assert.notCalled(prebidServerAdapterMock.callBids); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call client adapters for bidders that go to server', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.SERVER; + adUnits[0].bids[1].finalSource = s2sTesting.SERVER; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + callBids(adUnits); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client and server adapters for bidders that go to both', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.BOTH; + adUnits[0].bids[1].finalSource = s2sTesting.BOTH; + adUnits[1].bids[0].finalSource = s2sTesting.BOTH; + adUnits[1].bids[1].finalSource = s2sTesting.BOTH; + callBids(adUnits); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('makes mixed client/server adapter calls for mixed bidder sources', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + callBids(adUnits); + + // server adapter + checkServerCalled(1, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 1); + + // adequant + checkClientCalled(adequantAdapterMock, 1); + }); + }); + describe('aliasBidderAdaptor', function() { const CODE = 'sampleBidder'; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 392b0087099..9cded6fd3e4 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1590,42 +1590,4 @@ describe('Unit: Prebid Module', function () { assert.equal($$PREBID_GLOBAL$$.que.push, $$PREBID_GLOBAL$$.cmd.push); }); }); - - describe('setS2SConfig', () => { - let logErrorSpy; - - beforeEach(() => { - logErrorSpy = sinon.spy(utils, 'logError'); - }); - - afterEach(() => { - utils.logError.restore(); - }); - - it('should log error when accountId is missing', () => { - const options = { - enabled: true, - bidders: ['appnexus'], - timeout: 1000, - adapter: 'prebidServer', - endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' - }; - - $$PREBID_GLOBAL$$.setConfig({ s2sConfig: {options} }); - assert.ok(logErrorSpy.calledOnce, true); - }); - - it('should log error when bidders is missing', () => { - const options = { - accountId: '1', - enabled: true, - timeout: 1000, - adapter: 's2s', - endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' - }; - - $$PREBID_GLOBAL$$.setConfig({ s2sConfig: {options} }); - assert.ok(logErrorSpy.calledOnce, true); - }); - }); });