From 9044c27dc2bbf106aa0cf6b75c7b7b579a70e4cd Mon Sep 17 00:00:00 2001 From: harpere Date: Tue, 17 Oct 2017 13:54:13 -0400 Subject: [PATCH] Rubicon feature/s2s test module (#1678) * added new server-to-server testing module * fixed linting errors * s2sTesting module fixes * s2sTesting module fixes * removed "both" option from s2s ab testing module * removed leftover StorageManager reference * s2sTesting module improvements * s2sTesting module improvement * Revert "Merge branch 'bugfix/pbs-adapter-storagemanager' into rubicon-feature/s2s-test-module" This reverts commit be232c62a5fdc3c99ea49be778f490ad238789b1, reversing changes made to 4a3abd771c1e0b574f4a750525efa23b5c2a32f5. * minor s2sTesting fixes --- modules/s2sTesting.js | 126 ++++++ src/adaptermanager.js | 48 ++- test/spec/modules/s2sTesting_spec.js | 437 +++++++++++++++++++++ test/spec/unit/core/adapterManager_spec.js | 189 ++++++++- 4 files changed, 788 insertions(+), 12 deletions(-) create mode 100644 modules/s2sTesting.js create mode 100644 test/spec/modules/s2sTesting_spec.js diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js new file mode 100644 index 000000000000..a821383dc2db --- /dev/null +++ b/modules/s2sTesting.js @@ -0,0 +1,126 @@ +import { config } from 'src/config'; +import { setS2STestingModule } from 'src/adaptermanager'; + +var CONSTANTS = require('src/constants.json'); +const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; +export const SERVER = 'server'; +export const CLIENT = 'client'; + +var testing = false; // whether testing is turned on +var bidSource = {}; // store bidder sources determined from s2sConfing bidderControl + +// load s2sConfig +config.getConfig('s2sConfig', config => { + testing = config.s2sConfig && config.s2sConfig.testing; + addBidderSourceTargeting(config.s2sConfig) + calculateBidSources(config.s2sConfig); +}); + +// function to add hb_source_ adServerTargeting (AST) kvp to bidder settings +function addBidderSourceTargeting(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + var bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; + var bidderControl = s2sConfig.bidderControl || {}; + // for each configured bidder + (s2sConfig.bidders || []).forEach((bidder) => { + // remove any existing kvp setting + if (bidderSettings[bidder] && bidderSettings[bidder][AST]) { + bidderSettings[bidder][AST] = bidderSettings[bidder][AST].filter((kvp) => { + return kvp.key !== `hb_source_${bidder}`; + }); + } + // if includeSourceKvp === true add new kvp setting + if (bidderControl[bidder] && bidderControl[bidder].includeSourceKvp) { + bidderSettings[bidder] = bidderSettings[bidder] || {}; + bidderSettings[bidder][AST] = bidderSettings[bidder][AST] || []; + bidderSettings[bidder][AST].push({ + key: `hb_source_${bidder}`, + val: function (bidResponse) { + // default to client (currently only S2S sets this) + return bidResponse.source || CLIENT; + } + }); + // make sure "alwaysUseBid" is true so targeting is set + bidderSettings[bidder].alwaysUseBid = true; + } + }); +} + +export function getSourceBidderMap(adUnits = []) { + var sourceBidders = {[SERVER]: {}, [CLIENT]: {}}; + + // bail if testing is not turned on + if (!testing) { + return {[SERVER]: [], [CLIENT]: []}; + } + + adUnits.forEach((adUnit) => { + // if any adUnit bidders specify a bidSource, include them + (adUnit.bids || []).forEach((bid) => { + // calculate the source once and store on bid object + bid.calcSource = bid.calcSource || getSource(bid.bidSource); + // if no bidSource at bid level, default to bidSource from bidder + bid.finalSource = bid.calcSource || bidSource[bid.bidder] || CLIENT; // default to client + // add bidder to sourceBidders data structure + sourceBidders[bid.finalSource][bid.bidder] = true; + }); + }); + + // make sure all bidders in bidSource are in sourceBidders + Object.keys(bidSource).forEach((bidder) => { + sourceBidders[bidSource[bidder]][bidder] = true; + }); + + // return map of source => array of bidders + return { + [SERVER]: Object.keys(sourceBidders[SERVER]), + [CLIENT]: Object.keys(sourceBidders[CLIENT]) + }; +} + +/** + * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level + * @param s2sConfig server-to-server configuration + */ +function calculateBidSources(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + bidSource = {}; // reset bid sources + // calculate bid source (server/client) for each s2s bidder + var bidderControl = s2sConfig.bidderControl || {}; + (s2sConfig.bidders || []).forEach((bidder) => { + bidSource[bidder] = getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server + }); +} + +/** + * @function getSource() gets a random source based on the given sourceWeights (export just for testing) + * @param sourceWeights mapping of relative weights of potential sources. for example {server: 1, client: 3} should do a server request 25% of the time and a client request 75% of the time. + * @param bidSources list of possible bid sources: "server", "client". In theory could get the sources from the sourceWeights keys, but this is publisher config defined, so bidSources let's us constrain that. + * @return the chosen source ("server" or "client"), or undefined if none chosen + */ +export function getSource(sourceWeights = {}, bidSources = [SERVER, CLIENT]) { + var srcIncWeight = {}; // store incremental weights of each source + var totWeight = 0; + bidSources.forEach((source) => { + totWeight += (sourceWeights[source] || 0); + srcIncWeight[source] = totWeight; + }); + if (!totWeight) return; // bail if no source weights + // choose a source randomly based on weights + var rndWeight = Math.random() * totWeight; + for (var i = 0; i < bidSources.length; i++) { + let source = bidSources[i]; + // choose the first source with an incremental weight > random weight + if (rndWeight < srcIncWeight[source]) return source; + } +} + +// inject the s2sTesting module into the adaptermanager rather than importing it +// importing it causes the packager to include it even when it's not explicitly included in the build +setS2STestingModule(exports); diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 48c320548a0c..2fa3e6de3b98 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -8,6 +8,7 @@ import { newBidder } from './adapters/bidderFactory'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); var events = require('./events'); +let s2sTestingModule; // store s2sTesting module if it's loaded var _bidderRegistry = {}; exports.bidderRegistry = _bidderRegistry; @@ -100,25 +101,34 @@ exports.callBids = ({adUnits, cbTimeout}) => { s2sAdapter.queueSync({bidderCodes}); } + let clientTestAdapters = []; + let s2sTesting = false; if (_s2sConfig.enabled) { + // if s2sConfig.bidderControl testing is turned on + s2sTesting = _s2sConfig.testing && typeof s2sTestingModule !== 'undefined'; + if (s2sTesting) { + // get all adapters doing client testing + clientTestAdapters = s2sTestingModule.getSourceBidderMap(adUnits)[s2sTestingModule.CLIENT]; + } + // these are called on the s2s adapter let adaptersServerSide = _s2sConfig.bidders; - // don't call these client side + // don't call these client side (unless client request is needed for testing) bidderCodes = bidderCodes.filter((elm) => { - return !adaptersServerSide.includes(elm); + return !adaptersServerSide.includes(elm) || clientTestAdapters.includes(elm); }); - let adUnitsCopy = utils.cloneJson(adUnits); + let adUnitsS2SCopy = utils.cloneJson(adUnits); // filter out client side bids - adUnitsCopy.forEach((adUnit) => { + adUnitsS2SCopy.forEach((adUnit) => { if (adUnit.sizeMapping) { adUnit.sizes = mapSizes(adUnit); delete adUnit.sizeMapping; } adUnit.sizes = transformHeightWidth(adUnit); adUnit.bids = adUnit.bids.filter((bid) => { - return adaptersServerSide.includes(bid.bidder); + return adaptersServerSide.includes(bid.bidder) && (!s2sTesting || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; @@ -126,7 +136,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { }); // don't send empty requests - adUnitsCopy = adUnitsCopy.filter(adUnit => { + adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnit => { return adUnit.bids.length !== 0; }); @@ -138,7 +148,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { requestId, bidderRequestId, tid, - bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsCopy}), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsS2SCopy}), start: new Date().getTime(), auctionStart: auctionStart, timeout: _s2sConfig.timeout, @@ -149,13 +159,27 @@ exports.callBids = ({adUnits, cbTimeout}) => { } }); - let s2sBidRequest = {tid, 'ad_units': adUnitsCopy}; + let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy}; utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.join(',')}`); if (s2sBidRequest.ad_units.length) { s2sAdapter.callBids(s2sBidRequest); } } + // 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 => { const adapter = _bidderRegistry[bidderCode]; if (adapter) { @@ -164,7 +188,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { bidderCode, requestId, bidderRequestId, - bids: getBids({bidderCode, requestId, bidderRequestId, adUnits}), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsClientCopy}), start: new Date().getTime(), auctionStart: auctionStart, timeout: cbTimeout @@ -296,3 +320,9 @@ exports.setBidderSequence = function (order) { exports.setS2SConfig = function (config) { _s2sConfig = config; }; + +// 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) { + s2sTestingModule = module; +}; diff --git a/test/spec/modules/s2sTesting_spec.js b/test/spec/modules/s2sTesting_spec.js new file mode 100644 index 000000000000..f829087a9675 --- /dev/null +++ b/test/spec/modules/s2sTesting_spec.js @@ -0,0 +1,437 @@ +import { getSourceBidderMap, calculateBidSources, getSource } from 'modules/s2sTesting'; +import { config } from 'src/config'; + +var events = require('src/events'); +var CONSTANTS = require('src/constants.json'); +const BID_ADJUSTMENT = CONSTANTS.EVENTS.BID_ADJUSTMENT; + +var expect = require('chai').expect; + +describe('s2sTesting', function () { + let mathRandomStub; + let randomNumber = 0; + + beforeEach(() => { + mathRandomStub = sinon.stub(Math, 'random', () => { return randomNumber; }); + }); + + afterEach(() => { + mathRandomStub.restore(); + }); + + describe('getSource', () => { + // helper function to set random number and get the source + function getExpectedSource(randNumber, sourceWeights, sources) { + // set random number for testing + randomNumber = randNumber; + return getSource(sourceWeights, sources); + } + + it('returns undefined if no sources', () => { + expect(getExpectedSource(0, {})).to.be.undefined; + expect(getExpectedSource(0.5, {})).to.be.undefined; + expect(getExpectedSource(0.9999, {})).to.be.undefined; + }); + + it('returns undefined if no weights', () => { + expect(getExpectedSource(0, {server: 0, client: 0})).to.be.undefined; + expect(getExpectedSource(0.5, {client: 0})).to.be.undefined; + }); + + it('gets the expected source from 3 sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + expect(getExpectedSource(0.99999, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + }); + + it('gets the expected source from 2 sources', () => { + expect(getExpectedSource(0, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3})).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3})).to.equal('client'); + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3}, sources)).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3}, sources)).to.equal('client'); + }); + + it('gets the expected source from 1 source', () => { + expect(getExpectedSource(0, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.5, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.99999, {client: 2})).to.equal('client'); + }); + + it('ignores an invalid source', () => { + expect(getExpectedSource(0, {client: 2, cache: 2})).to.equal('client'); + expect(getExpectedSource(0.3333, {server: 1, cache: 1, client: 2})).to.equal('server'); + expect(getExpectedSource(0.34, {server: 1, cache: 1, client: 2})).to.equal('client'); + }); + + it('ignores order of sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {client: 1, server: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {both: 2, client: 1, server: 1}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {client: 1, both: 2, server: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, both: 2, client: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {both: 2, server: 1, client: 1}, sources)).to.equal('both'); + }); + + it('accepts an array of sources', () => { + expect(getExpectedSource(0.3333, {second: 2, first: 1}, ['first', 'second'])).to.equal('first'); + expect(getExpectedSource(0.34, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + expect(getExpectedSource(0.9999, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + }); + }); + + describe('getSourceBidderMap', () => { + describe('setting source through s2sConfig', () => { + beforeEach(() => { + // set random number for testing + randomNumber = 0.7; + }); + + it('does not work if testing is "false"', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: false, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: [] + }); + }); + + it('sets one client bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: ['rubicon'] + }); + }); + + it('sets one server bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 4, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('defaults to server', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('sets two bidders', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 3, client: 1}}, + appnexus: {bidSource: {server: 1, client: 1}} + }}}); + var serverClientBidders = getSourceBidderMap(); + expect(serverClientBidders.server).to.eql(['rubicon']); + expect(serverClientBidders.client).to.have.members(['appnexus']); + }); + }); + + describe('setting source through adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('sets one bidder source from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: ['rubicon'], + client: [] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('server'); + expect(adUnits[0].bids[0].finalSource).to.equal('server'); + + adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 1, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('defaults to client if no bidSource', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.be.undefined; + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('sets multiple bidders sources from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 3, client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.eql(['appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + }); + + it('sets multiple bidders sources from multiple adUnits', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 1, client: 1}} + ]}, + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'bidder3', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon']); + expect(serverClientBidders.server).to.not.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('client'); + expect(adUnits[0].bids[1].finalSource).to.equal('client'); + expect(adUnits[1].bids[0].calcSource).to.equal('server'); + expect(adUnits[1].bids[0].finalSource).to.equal('server'); + expect(adUnits[1].bids[1].calcSource).to.equal('client'); + expect(adUnits[1].bids[1].finalSource).to.equal('client'); + }); + + it('should reuse calculated sources', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', calcSource: 'client', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', calcSource: 'server', bidSource: {server: 1, client: 1}}, + {bidder: 'bidder3', calcSource: 'server', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + + expect(serverClientBidders.server).to.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.server).to.not.have.members(['rubicon']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + expect(serverClientBidders.client).to.not.have.members(['appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + expect(adUnits[0].bids[2].calcSource).to.equal('server'); + expect(adUnits[0].bids[2].finalSource).to.equal('server'); + }); + }); + + describe('setting source through s2sconfig and adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('should get sources from both', () => { + // set rubicon: server and appnexus: client + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', bidSource: {client: 1}} + ]} + ]; + + // set rubicon: client and appnexus: server + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon', 'appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus']); + }); + }); + }); + + describe('addBidderSourceTargeting', () => { + const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; + + function checkTargeting(bidder) { + var targeting = window.pbjs.bidderSettings[bidder][AST]; + var srcTargeting = targeting[targeting.length - 1]; + expect(srcTargeting.key).to.equal(`hb_source_${bidder}`); + expect(srcTargeting.val).to.be.a('function'); + expect(window.pbjs.bidderSettings[bidder].alwaysUseBid).to.be.true; + } + + function checkNoTargeting(bidder) { + var bs = window.pbjs.bidderSettings; + var targeting = bs[bidder] && bs[bidder][AST]; + if (!targeting) { + expect(targeting).to.be.undefined; + return; + } + expect(targeting.find((kvp) => { + return kvp.key === `hb_source_${bidder}`; + })).to.be.undefined; + } + + function checkTargetingVal(bidResponse, expectedVal) { + var targeting = window.pbjs.bidderSettings[bidResponse.bidderCode][AST]; + var targetingFunc = targeting[targeting.length - 1].val; + expect(targetingFunc(bidResponse)).to.equal(expectedVal); + } + + beforeEach(() => { + // set bidderSettings + window.pbjs.bidderSettings = {}; + }); + + it('should not set hb_source_ unless testing is on and includeSourceKvp is set', () => { + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus']}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus'], testing: true}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: false, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + }); + + it('should set hb_source_ if includeSourceKvp is set', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'server'}, 'server'); + checkTargetingVal({bidderCode: 'appnexus', source: 'client'}, 'client'); + + // turn off appnexus + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: false} + } + }}); + checkTargeting('rubicon'); + checkNoTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'client'}, 'client'); + + // should default to "client" + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon'}, 'client'); + checkTargetingVal({bidderCode: 'appnexus'}, 'client'); + }); + + it('should reset adServerTargeting when a new config is set', () => { + // set config with targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + + // set config without targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true + }}); + checkNoTargeting('rubicon'); + checkNoTargeting('appnexus'); + }); + }); +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 10bd60740217..6da22ed89845 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -4,6 +4,7 @@ import { getAdUnits } from 'test/fixtures/fixtures'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; +var s2sTesting = require('../../../../modules/s2sTesting'); const CONFIG = { enabled: true, @@ -19,12 +20,21 @@ var prebidServerAdapterMock = { setConfig: sinon.stub(), queueSync: sinon.stub() }; +var adequantAdapterMock = { + bidder: 'adequant', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; +var appnexusAdapterMock = { + bidder: 'appnexus', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; describe('adapterManager tests', () => { describe('S2S tests', () => { - var stubGetStorageItem; - var stubSetStorageItem; - beforeEach(() => { AdapterManager.setS2SConfig(CONFIG); AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; @@ -83,6 +93,179 @@ describe('adapterManager tests', () => { }); }) + describe('s2sTesting', () => { + function getTestAdUnits() { + // copy adUnits + return JSON.parse(JSON.stringify(getAdUnits())); + } + + 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 + }); + + AdapterManager.setS2SConfig(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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // 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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // 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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // 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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + 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; + AdapterManager.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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + 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; + AdapterManager.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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + 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; + AdapterManager.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]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + 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; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(1, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 1); + + // adequant + checkClientCalled(adequantAdapterMock, 1); + }); + }); + describe('aliasBidderAdaptor', function() { const CODE = 'sampleBidder';