From 04e41b58fcf84ff01343c7ad78e8ac3883dffd08 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Mon, 13 Nov 2017 15:57:48 -0500 Subject: [PATCH 01/16] fix adaptermanager s2sTest unit tests --- src/adaptermanager.js | 69 +++++--- test/spec/unit/core/adapterManager_spec.js | 185 +++++++++++++++++++++ 2 files changed, 229 insertions(+), 25 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 8aefc67cad4..fdecc96a505 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 = {}; @@ -72,15 +75,15 @@ 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; @@ -94,6 +97,25 @@ 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; +} + +// ericeric: why is this broken out into a public function? why not just call from callBids()? +// I see auction.js is tracking _bidderRequests, but maybe callBids can just return them directly? exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout) { let bidRequests = []; let bidderCodes = getBidderCodes(adUnits); @@ -107,12 +129,11 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout) 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]; } @@ -121,11 +142,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(); @@ -145,27 +166,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}), + bids: getBids({bidderCode, auctionId, bidderRequestId, 'adUnits': adUnitsClientCopy}), auctionStart: auctionStart, timeout: cbTimeout }; @@ -177,6 +186,7 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout) } exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { + // handle s2s requests let serverBidRequests = bidRequests.filter(bidRequest => { return bidRequest.src && bidRequest.src === CONSTANTS.S2S.SRC; }); @@ -194,8 +204,13 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { } } } + + // handle client adapter requests let ajax = ajaxBuilder(bidRequests[0].timeout); - bidRequests.forEach(bidRequest => { + // first filter out s2s requests + bidRequests.filter(bidRequest => { + return !serverBidRequests.includes(bidRequest); + }).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]; @@ -211,6 +226,10 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { }); } +function doingS2STesting() { + return _s2sConfig && _s2sConfig.enabled && _s2sConfig.testing && s2sTestingModule; +} + function transformHeightWidth(adUnit) { let sizesObj = []; let sizes = utils.parseSizesInput(adUnit.sizes); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index ac4b2d3b587..3133e2f6f9c 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -251,6 +251,191 @@ describe('adapterManager tests', () => { }); }); // 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); + var 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); + } + + var TESTING_CONFIG; + var stubGetSourceBidderMap; + + beforeEach(() => { + TESTING_CONFIG = Object.assign(CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + + 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(() => { + 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'; From 19ee68057bd0bfe7d229ce6a9db44bf4d061e683 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 14 Nov 2017 13:52:06 -0500 Subject: [PATCH 02/16] fix s2s log message --- src/adaptermanager.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index fdecc96a505..c5c479d85ee 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -198,8 +198,15 @@ 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) { + // 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(',')}`); + // make bid requests s2sAdapter.callBids(s2sBidRequest); } } From 7ff136f9d89b28c1c94128ffb1126219508ababa Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 14 Nov 2017 13:56:49 -0500 Subject: [PATCH 03/16] remove errant comment --- src/adaptermanager.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index c5c479d85ee..202627db2c8 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -114,8 +114,6 @@ function getAdUnitCopyForClientAdapters(adUnits) { return adUnitsClientCopy; } -// ericeric: why is this broken out into a public function? why not just call from callBids()? -// I see auction.js is tracking _bidderRequests, but maybe callBids can just return them directly? exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout) { let bidRequests = []; let bidderCodes = getBidderCodes(adUnits); From a2e2db6d1b8e6a178d3621eb0fd7b7e71cddd35f Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Tue, 14 Nov 2017 16:19:22 -0500 Subject: [PATCH 04/16] fixed log statement --- src/adaptermanager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 9b26a92e587..b40065d4098 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -206,7 +206,7 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { if (!bidRequests.length) { - utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); + utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); return; } From 432f46665d67307961df9d303539ddd8ea13308c Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 15 Nov 2017 02:01:12 -0500 Subject: [PATCH 05/16] removed seemingly unnecessary call to transformHeightWidth(adUnit); --- src/adaptermanager.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index b40065d4098..d9bd2b31d67 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -101,7 +101,6 @@ function getAdUnitCopyForPrebidServer(adUnits) { 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) && (!doingS2STesting() || bid.finalSource !== s2sTestingModule.CLIENT); @@ -261,20 +260,6 @@ function doingS2STesting() { return _s2sConfig && _s2sConfig.enabled && _s2sConfig.testing && s2sTestingModule; } -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 getSupportedMediaTypes(bidderCode) { let result = []; if (exports.videoAdapters.includes(bidderCode)) result.push('video'); From dcc83e560ecb8422cab4c316abcd8759996f2412 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 15 Nov 2017 11:32:22 -0500 Subject: [PATCH 06/16] removed legacy sizeMapping code block --- src/adaptermanager.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index d9bd2b31d67..8bdb4027666 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -97,10 +97,6 @@ function getAdUnitCopyForPrebidServer(adUnits) { let adUnitsCopy = utils.cloneJson(adUnits); adUnitsCopy.forEach((adUnit) => { - if (adUnit.sizeMapping) { - adUnit.sizes = mapSizes(adUnit); - delete adUnit.sizeMapping; - } // filter out client side bids adUnit.bids = adUnit.bids.filter((bid) => { return adaptersServerSide.includes(bid.bidder) && (!doingS2STesting() || bid.finalSource !== s2sTestingModule.CLIENT); From 5be999ed80ff66d953a8ed5d7e83f3d2b64b5979 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 15 Nov 2017 13:29:45 -0700 Subject: [PATCH 07/16] initial refactor of prebidServerBidAdapter working w/o tests (cherry picked from commit 2b843d0) --- modules/prebidServerBidAdapter.js | 342 ++++++++++++++++++++++++++++++ src/adaptermanager.js | 40 ++-- src/config.js | 28 --- src/utils.js | 2 +- test/spec/unit/pbjs_api_spec.js | 74 +++---- 5 files changed, 404 insertions(+), 82 deletions(-) create mode 100644 modules/prebidServerBidAdapter.js diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js new file mode 100644 index 00000000000..1de361af609 --- /dev/null +++ b/modules/prebidServerBidAdapter.js @@ -0,0 +1,342 @@ +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'; +import CONSTANTS from 'src/constants.json'; + +const getConfig = config.getConfig; + +const TYPE = S2S.SRC; +const cookieSetUrl = 'https://acdn.adnxs.com/cookieset/cs.js'; +let _synced = false; + +let _s2sConfigDefaults = { + enabled: false, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, + timeout: 1000, + maxBids: 1, + adapter: CONSTANTS.S2S.ADAPTER, + syncEndpoint: CONSTANTS.S2S.SYNC_ENDPOINT, + cookieSet: true, + bidders: [] +}; +let _s2sConfig = _s2sConfigDefaults; + +/** + * 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 + * === optional params below === + * @property {string} [endpoint] endpoint to contact + * @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 {boolean} [cookieSet] enables cookieSet functionality + */ +export function setS2sConfig(options) { + let keys = Object.keys(options); + if (!keys.includes('accountId')) { + utils.logError('accountId missing in Server to Server config'); + return; + } + + if (!keys.includes('bidders')) { + utils.logError('bidders missing in Server to Server config'); + return; + } + _s2sConfig = Object.assign({}, _s2sConfigDefaults, 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 + }); +} + +/** + * 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 + }, +}; + +let _cookiesQueued = false; + +/** + * 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]) { + const converted = types[key](bid.params[key]); + if (converted !== bid.params[key]) { + utils.logMessage(`Mismatched type for Prebid Server : ${bid.bidder} : ${key}. Required Type:${types[key]}`); + } + bid.params[key] = converted; + + // 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(bidRequest, bidRequests, addBidResponse, done, ajax) { + const isDebug = !!getConfig('debug'); + const adUnits = utils.cloneJson(bidRequest.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: bidRequest.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; + } + + /** + * 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}"`); + } + } + + /* 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 && !_cookiesQueued) { + 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); + }); + } + + // const receivedBidIds = result.bids ? result.bids.map(bidObj => bidObj.bid_id) : []; + + // issue a no-bid response for every bid request that can not be matched with received bids + // requestedBidders.forEach(bidder => { + // utils + // .getBidderRequestAllAdUnits(bidder) + // .bids.filter(bidRequest => !receivedBidIds.includes(bidRequest.bidId)) + // .forEach(bidRequest => { + // let bidObject = bidfactory.createBid(STATUS.NO_BID, bidRequest); + // bidObject.source = TYPE; + // bidObject.adUnitCode = bidRequest.placementCode; + // bidObject.bidderCode = bidRequest.bidder; + // addBidResponse(bidObject.adUnitCode, bidObject); + // }); + // }); + } + if (result.status === 'no_cookie' && _s2sConfig.cookieSet) { + // cookie sync + cookieSet(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 8bdb4027666..790a59b4191 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -137,12 +137,6 @@ 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 = []; if (_s2sConfig.enabled) { @@ -205,10 +199,12 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { return; } - // handle s2s requests - let serverBidRequests = bidRequests.filter(bidRequest => { - return bidRequest.src && bidRequest.src === CONSTANTS.S2S.SRC; - }); + 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; @@ -218,6 +214,11 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { if (s2sAdapter) { let s2sBidRequest = {tid, 'ad_units': getAdUnitCopyForPrebidServer(adUnits)}; if (s2sBidRequest.ad_units.length) { + 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) }, [])); @@ -225,18 +226,21 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => { return allBidders.includes(adapter); }).join(',')}`); + // make bid requests - s2sAdapter.callBids(s2sBidRequest); + s2sAdapter.callBids( + s2sBidRequest, + serverBidRequests, + addBidResponse, + () => doneCbs.forEach(done => done()), + ajax + ); } } } // handle client adapter requests - let ajax = ajaxBuilder(bidRequests[0].timeout); - // first filter out s2s requests - bidRequests.filter(bidRequest => { - return !serverBidRequests.includes(bidRequest); - }).forEach(bidRequest => { + 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]; @@ -346,6 +350,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..b5a2627730a 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'; @@ -152,24 +142,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 }; diff --git a/src/utils.js b/src/utils.js index 9c29e233380..9cfa83dae6e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -773,7 +773,7 @@ export function getBidderRequest(bidRequests, bidder, adUnitCode) { } /** - * Returns the origin + * Returns the origin */ export function getOrigin() { // IE10 does not have this propery. https://gist.github.com/hbogs/7908703 diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 613074e59f9..7622cafa6e1 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1592,41 +1592,41 @@ describe('Unit: Prebid Module', function () { }); }); - 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); - }); - }); + // 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); + // }); + // }); }); From e35c0a8262fbbb8200fc3a3b162bf55fdd805242 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 15 Nov 2017 15:39:06 -0700 Subject: [PATCH 08/16] add transformSizes back for prebidServer adUnits to fix request --- src/adaptermanager.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 790a59b4191..ba3c320463e 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -92,11 +92,27 @@ 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); adUnitsCopy.forEach((adUnit) => { + adUnit.sizes = transformHeightWidth(adUnit); + // filter out client side bids adUnit.bids = adUnit.bids.filter((bid) => { return adaptersServerSide.includes(bid.bidder) && (!doingS2STesting() || bid.finalSource !== s2sTestingModule.CLIENT); From b6ecc505b1343b82762f63d45ad0e62af872e0bc Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Thu, 16 Nov 2017 03:12:57 -0500 Subject: [PATCH 09/16] fixed adapterManager_spec tests --- test/spec/unit/core/adapterManager_spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 869681818fb..b1bc8b66a1a 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -265,7 +265,7 @@ describe('adapterManager tests', () => { function checkServerCalled(numAdUnits, numBids) { sinon.assert.calledOnce(prebidServerAdapterMock.callBids); - var requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + 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) => { @@ -279,15 +279,14 @@ describe('adapterManager tests', () => { expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); } - var TESTING_CONFIG; - var stubGetSourceBidderMap; + let TESTING_CONFIG = utils.cloneJson(CONFIG); + Object.assign(TESTING_CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + let stubGetSourceBidderMap; beforeEach(() => { - TESTING_CONFIG = Object.assign(CONFIG, { - bidders: ['appnexus', 'adequant'], - testing: true - }); - config.setConfig({s2sConfig: TESTING_CONFIG}); AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; AdapterManager.bidderRegistry['adequant'] = adequantAdapterMock; @@ -301,6 +300,7 @@ describe('adapterManager tests', () => { }); afterEach(() => { + config.setConfig({s2sConfig: {}}); s2sTesting.getSourceBidderMap.restore(); }); From d0c0fd0eec80a2748ee0709c3a1722226e7bfbf6 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Thu, 16 Nov 2017 11:10:49 -0700 Subject: [PATCH 10/16] added prebidServerBidAdapter tests for 1.0 --- modules/prebidServerBidAdapter.js | 6 +- .../modules/prebidServerBidAdapter_spec.js | 443 ++++++++++++++++++ 2 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 test/spec/modules/prebidServerBidAdapter_spec.js diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 1de361af609..f0c3103cc28 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -167,9 +167,9 @@ export function PrebidServer() { } /* Prebid executes this function when the page asks to send out bid requests */ - baseAdapter.callBids = function(bidRequest, bidRequests, addBidResponse, done, ajax) { + baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { const isDebug = !!getConfig('debug'); - const adUnits = utils.cloneJson(bidRequest.ad_units); + const adUnits = utils.cloneJson(s2sBidRequest.ad_units); adUnits.forEach(adUnit => { let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video'); if (videoMediaType) { @@ -183,7 +183,7 @@ export function PrebidServer() { convertTypes(adUnits); let requestJson = { account_id: _s2sConfig.accountId, - tid: bidRequest.tid, + tid: s2sBidRequest.tid, max_bids: _s2sConfig.maxBids, timeout_millis: _s2sConfig.timeout, secure: _s2sConfig.secure, diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js new file mode 100644 index 00000000000..376710cdf4f --- /dev/null +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -0,0 +1,443 @@ +import { expect } from 'chai'; +import { PrebidServer as Adapter, setS2sConfig } from 'modules/prebidServerBidAdapter'; +import adapterManager from 'src/adaptermanager'; +import CONSTANTS from 'src/constants.json'; +import * as utils from 'src/utils'; +import cookie from 'src/cookie'; +import { userSync } from 'src/userSync'; +import { ajax } from 'src/ajax'; + +let CONFIG = { + accountId: '1', + enabled: true, + bidders: ['appnexus'], + timeout: 1000, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT +}; + +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', () => { + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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)); + + setS2sConfig(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 config = Object.assign({ + cookieSet: false + }, CONFIG); + + setS2sConfig(config); + 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 config = Object.assign({ + cookieSet: true + }, CONFIG); + + setS2sConfig(config); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + sinon.assert.calledOnce(cookie.cookieSet); + }); + }); +}); From 6274573cc739dcbcb8a7b77fd88e28bbe719d69b Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Thu, 16 Nov 2017 13:48:07 -0500 Subject: [PATCH 11/16] fixed lint errors --- test/spec/modules/prebidServerBidAdapter_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 376710cdf4f..4b8aa3cf49a 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -63,7 +63,7 @@ const BID_REQUESTS = [ 'params': { 'placementId': '10433394', 'member': 123 - }, + }, 'bid_id': '123', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', @@ -216,8 +216,8 @@ describe('S2S Adapter', () => { beforeEach(() => adapter = new Adapter()); afterEach(() => { - addBidResponse.reset(); - done.reset(); + addBidResponse.reset(); + done.reset(); }); describe('request function', () => { From aba436efae133adb6dbc0c997e9fc2436dac4337 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Thu, 16 Nov 2017 15:20:52 -0700 Subject: [PATCH 12/16] make sure addBidResponse and doneCb are stubbed for s2s calls --- test/spec/unit/core/adapterManager_spec.js | 31 ++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index b1bc8b66a1a..1a8f37518fc 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -18,21 +18,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', () => { @@ -106,8 +100,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 +159,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,7 +243,12 @@ 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); From a8c6cbfd44bd5a0331fc6e68ce3fc42f23b12161 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 22 Nov 2017 00:38:20 -0500 Subject: [PATCH 13/16] s2s requests now firing BID_REQUESTED event --- src/adaptermanager.js | 5 +++ test/spec/unit/core/adapterManager_spec.js | 51 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index ba3c320463e..a009e91cd24 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -243,6 +243,11 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { 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, diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 1a8f37518fc..1283fa3a75b 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, @@ -90,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', () => { @@ -253,6 +270,40 @@ describe('adapterManager tests', () => { 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', () => { From 9e6ddc6963b16c2882e38e33c8af7e9b95b2439e Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 22 Nov 2017 12:36:47 -0500 Subject: [PATCH 14/16] fixed commented tests and other minor fixes --- modules/prebidServerBidAdapter.js | 28 +------ .../modules/prebidServerBidAdapter_spec.js | 73 ++++++++++++++----- test/spec/unit/pbjs_api_spec.js | 38 ---------- 3 files changed, 59 insertions(+), 80 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index f0c3103cc28..6f6ebbfe217 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -39,7 +39,7 @@ let _s2sConfig = _s2sConfigDefaults; * @property {string} [syncEndpoint] endpoint URL for syncing cookies * @property {boolean} [cookieSet] enables cookieSet functionality */ -export function setS2sConfig(options) { +function setS2sConfig(options) { let keys = Object.keys(options); if (!keys.includes('accountId')) { utils.logError('accountId missing in Server to Server config'); @@ -136,8 +136,6 @@ const paramTypes = { }, }; -let _cookiesQueued = false; - /** * Bidder adapter for Prebid Server */ @@ -150,11 +148,7 @@ export function PrebidServer() { const types = paramTypes[bid.bidder] || []; Object.keys(types).forEach(key => { if (bid.params[key]) { - const converted = types[key](bid.params[key]); - if (converted !== bid.params[key]) { - utils.logMessage(`Mismatched type for Prebid Server : ${bid.bidder} : ${key}. Required Type:${types[key]}`); - } - bid.params[key] = converted; + bid.params[key] = types[key](bid.params[key]); // don't send invalid values if (isNaN(bid.params[key])) { @@ -240,7 +234,7 @@ export function PrebidServer() { if (result.status === 'OK' || result.status === 'no_cookie') { if (result.bidder_status) { result.bidder_status.forEach(bidder => { - if (bidder.no_cookie && !_cookiesQueued) { + if (bidder.no_cookie) { doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); } }); @@ -300,22 +294,6 @@ export function PrebidServer() { addBidResponse(bidObj.code, bidObject); }); } - - // const receivedBidIds = result.bids ? result.bids.map(bidObj => bidObj.bid_id) : []; - - // issue a no-bid response for every bid request that can not be matched with received bids - // requestedBidders.forEach(bidder => { - // utils - // .getBidderRequestAllAdUnits(bidder) - // .bids.filter(bidRequest => !receivedBidIds.includes(bidRequest.bidId)) - // .forEach(bidRequest => { - // let bidObject = bidfactory.createBid(STATUS.NO_BID, bidRequest); - // bidObject.source = TYPE; - // bidObject.adUnitCode = bidRequest.placementCode; - // bidObject.bidderCode = bidRequest.bidder; - // addBidResponse(bidObject.adUnitCode, bidObject); - // }); - // }); } if (result.status === 'no_cookie' && _s2sConfig.cookieSet) { // cookie sync diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 4b8aa3cf49a..ed491c9aa64 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,11 +1,12 @@ import { expect } from 'chai'; -import { PrebidServer as Adapter, setS2sConfig } from 'modules/prebidServerBidAdapter'; +import { PrebidServer as Adapter } from 'modules/prebidServerBidAdapter'; import adapterManager from 'src/adaptermanager'; import CONSTANTS from 'src/constants.json'; 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', @@ -237,7 +238,7 @@ describe('S2S Adapter', () => { }); it('exists converts types', () => { - setS2sConfig(CONFIG); + 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'); @@ -272,7 +273,7 @@ describe('S2S Adapter', () => { it('registers bids', () => { server.respondWith(JSON.stringify(RESPONSE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); sinon.assert.calledOnce(addBidResponse); @@ -286,7 +287,7 @@ describe('S2S Adapter', () => { it('does not call addBidResponse and calls done when ad unit not set', () => { server.respondWith(JSON.stringify(RESPONSE_NO_BID_NO_UNIT)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -297,7 +298,7 @@ describe('S2S Adapter', () => { it('does not call addBidResponse and calls done when server requests cookie sync', () => { server.respondWith(JSON.stringify(RESPONSE_NO_COOKIE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -308,7 +309,7 @@ describe('S2S Adapter', () => { it('does not call addBidResponse and calls done when ad unit is set', () => { server.respondWith(JSON.stringify(RESPONSE_NO_BID_UNIT_SET)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -319,7 +320,7 @@ describe('S2S Adapter', () => { it('registers successful bids and calls done when there are less bids than requests', () => { server.respondWith(JSON.stringify(RESPONSE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -337,7 +338,7 @@ describe('S2S Adapter', () => { it('should have dealId in bidObject', () => { server.respondWith(JSON.stringify(RESPONSE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); const response = addBidResponse.firstCall.args[1]; @@ -347,7 +348,7 @@ describe('S2S Adapter', () => { it('should pass through default adserverTargeting if present in bidObject', () => { server.respondWith(JSON.stringify(RESPONSE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); const response = addBidResponse.firstCall.args[1]; @@ -362,7 +363,7 @@ describe('S2S Adapter', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -374,7 +375,7 @@ describe('S2S Adapter', () => { it('registers bid responses when server requests cookie sync', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); sinon.assert.calledOnce(addBidResponse); @@ -393,7 +394,7 @@ describe('S2S Adapter', () => { it('does cookie sync when no_cookie response', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -406,7 +407,7 @@ describe('S2S Adapter', () => { it('logs error when no_cookie response is missing type or url', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR)); - setS2sConfig(CONFIG); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); @@ -418,11 +419,11 @@ describe('S2S Adapter', () => { it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - let config = Object.assign({ + let myConfig = Object.assign({ cookieSet: false }, CONFIG); - setS2sConfig(config); + config.setConfig({s2sConfig: myConfig}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.respond(); sinon.assert.notCalled(cookie.cookieSet); @@ -430,14 +431,52 @@ describe('S2S Adapter', () => { it('calls cookieSet cookie sync when no_cookie response && opted in', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - let config = Object.assign({ + let myConfig = Object.assign({ cookieSet: true }, CONFIG); - setS2sConfig(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/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 7622cafa6e1..185a82ec36b 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1591,42 +1591,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); - // }); - // }); }); From a579816737461716a33a82ba9cc32583eb953ce3 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Mon, 27 Nov 2017 16:35:19 -0700 Subject: [PATCH 15/16] update defaults in prebidServerBidAdapter and fix doBidderSync bug --- modules/prebidServerBidAdapter.js | 71 +++++++++---------- .../modules/prebidServerBidAdapter_spec.js | 9 +-- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 6f6ebbfe217..42318bbaf25 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -7,22 +7,17 @@ import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; import { VIDEO } from 'src/mediaTypes'; -import CONSTANTS from 'src/constants.json'; const getConfig = config.getConfig; const TYPE = S2S.SRC; -const cookieSetUrl = 'https://acdn.adnxs.com/cookieset/cs.js'; let _synced = false; let _s2sConfigDefaults = { enabled: false, - endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, timeout: 1000, maxBids: 1, - adapter: CONSTANTS.S2S.ADAPTER, - syncEndpoint: CONSTANTS.S2S.SYNC_ENDPOINT, - cookieSet: true, + adapter: 'prebidServer', bidders: [] }; let _s2sConfig = _s2sConfigDefaults; @@ -32,24 +27,26 @@ let _s2sConfig = _s2sConfigDefaults; * @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 {string} [endpoint] endpoint to contact * @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 {boolean} [cookieSet] enables cookieSet functionality + * @property {string} [cookieSetUrl] url for cookie set library, if passed then cookieSet is enabled */ function setS2sConfig(options) { let keys = Object.keys(options); - if (!keys.includes('accountId')) { - utils.logError('accountId missing in Server to Server config'); - return; - } - if (!keys.includes('bidders')) { - utils.logError('bidders missing in Server to Server config'); + 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 = Object.assign({}, _s2sConfigDefaults, options); if (options.syncEndpoint) { queueSync(options.bidders); @@ -83,6 +80,27 @@ function queueSync(bidderCodes) { }); } +/** + * 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. @@ -204,27 +222,6 @@ export function PrebidServer() { return unit.sizes && unit.sizes.length; } - /** - * 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}"`); - } - } - /* Notify Prebid of bid responses so bids can get in the auction */ function handleResponse(response, requestedBidders, bidRequests, addBidResponse, done) { let result; @@ -295,9 +292,9 @@ export function PrebidServer() { }); } } - if (result.status === 'no_cookie' && _s2sConfig.cookieSet) { + if (result.status === 'no_cookie' && typeof _s2sConfig.cookieSetUrl === 'string') { // cookie sync - cookieSet(cookieSetUrl); + cookieSet(_s2sConfig.cookieSetUrl); } } catch (error) { utils.logError(error); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index ed491c9aa64..608ac102ace 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { PrebidServer as Adapter } from 'modules/prebidServerBidAdapter'; import adapterManager from 'src/adaptermanager'; -import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; import cookie from 'src/cookie'; import { userSync } from 'src/userSync'; @@ -13,7 +12,7 @@ let CONFIG = { enabled: true, bidders: ['appnexus'], timeout: 1000, - endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT + endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' }; const REQUEST = { @@ -419,9 +418,7 @@ describe('S2S Adapter', () => { 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({ - cookieSet: false - }, CONFIG); + let myConfig = Object.assign({}, CONFIG); config.setConfig({s2sConfig: myConfig}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); @@ -432,7 +429,7 @@ describe('S2S Adapter', () => { it('calls cookieSet cookie sync when no_cookie response && opted in', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); let myConfig = Object.assign({ - cookieSet: true + cookieSetUrl: 'https://acdn.adnxs.com/cookieset/cs.js' }, CONFIG); config.setConfig({s2sConfig: myConfig}); From 09089febaee1d1d6fd36b70c8623d9d535720ba1 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Tue, 28 Nov 2017 10:48:41 -0700 Subject: [PATCH 16/16] add new API for setting defaults in config for modules --- modules/prebidServerBidAdapter.js | 19 +++++++++-------- src/config.js | 34 ++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 42318bbaf25..00c78e2d443 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -13,14 +13,15 @@ const getConfig = config.getConfig; const TYPE = S2S.SRC; let _synced = false; -let _s2sConfigDefaults = { - enabled: false, - timeout: 1000, - maxBids: 1, - adapter: 'prebidServer', - bidders: [] -}; -let _s2sConfig = _s2sConfigDefaults; +let _s2sConfig; +config.setDefaults({ + 's2sConfig': { + enabled: false, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer' + } +}); /** * Set config for server to server header bidding @@ -47,7 +48,7 @@ function setS2sConfig(options) { return; } - _s2sConfig = Object.assign({}, _s2sConfigDefaults, options); + _s2sConfig = options; if (options.syncEndpoint) { queueSync(options.bidders); } diff --git a/src/config.js b/src/config.js index b5a2627730a..9ff57777984 100644 --- a/src/config.js +++ b/src/config.js @@ -54,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, @@ -195,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); } /* @@ -264,7 +291,8 @@ export function newConfig() { return { getConfig, - setConfig + setConfig, + setDefaults }; }