UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,334 lines (1,163 loc) 132 kB
import {expect} from 'chai'; import { spec, getPriceGranularity, masSizeOrdering, resetUserSync, hasVideoMediaType, resetRubiConf } from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import find from 'core-js-pure/features/array/find.js'; import {createEidsArray} from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; describe('the rubicon adapter', function () { let sandbox, bidderRequest, sizeMap, getFloorResponse, logErrorSpy; /** * @typedef {Object} sizeMapConverted * @property {string} sizeId * @property {string} size * @property {Array.<Array>} sizeAsArray * @property {number} width * @property {number} height */ /** * @param {Array.<sizeMapConverted>} sizesMapConverted * @param {Object} bid * @return {sizeMapConverted} */ function getSizeIdForBid(sizesMapConverted, bid) { return find(sizesMapConverted, item => (item.width === bid.width && item.height === bid.height)); } /** * @param {Array.<Object>} ads * @param {sizeMapConverted} size * @return {Object} */ function getResponseAdBySize(ads, size) { return find(ads, item => item.size_id === size.sizeId); } /** * @param {Array.<BidRequest>} bidRequests * @param {sizeMapConverted} size * @return {BidRequest} */ function getBidRequestBySize(bidRequests, size) { return find(bidRequests, item => item.sizes[0][0] === size.width && item.sizes[0][1] === size.height); } /** * @typedef {Object} overrideProps * @property {string} status * @property {number} cpm * @property {number} zone_id * @property {number} ad_id * @property {string} creative_id * @property {string} targeting_key - rpfl_{id} */ /** * @param {number} i - index * @param {string} sizeId - id that maps to size * @param {Array.<overrideProps>} [indexOverMap] * @return {{status: string, cpm: number, zone_id: *, size_id: *, impression_id: *, ad_id: *, creative_id: string, type: string, targeting: *[]}} */ function getBidderRequest() { return { bidderCode: 'rubicon', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', bidderRequestId: '178e34bad3658f', bids: [ { bidder: 'rubicon', params: { accountId: '14062', siteId: '70608', zoneId: '335918', userId: '12346', keywords: ['a', 'b', 'c'], inventory: { rating: '5-star', // This actually should not be sent to frank!! causes 400 prodtype: ['tech', 'mobile'] }, visitor: { ucat: 'new', lastsearch: 'iphone', likes: ['sports', 'video games'] }, position: 'atf', referrer: 'localhost', latLong: [40.7607823, '111.8910325'] }, adUnitCode: '/19968336/header-bid-tag-0', code: 'div-1', sizes: [[300, 250], [320, 50]], bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' } ], start: 1472239426002, auctionStart: 1472239426000, timeout: 5000 }; }; function createResponseAdByIndex(i, sizeId, indexOverMap) { const overridePropMap = (indexOverMap && indexOverMap[i] && typeof indexOverMap[i] === 'object') ? indexOverMap[i] : {}; const overrideProps = Object.keys(overridePropMap).reduce((aggregate, key) => { aggregate[key] = overridePropMap[key]; return aggregate; }, {}); const getProp = (propName, defaultValue) => { return (overrideProps[propName]) ? overridePropMap[propName] : defaultValue; }; return { 'status': getProp('status', 'ok'), 'cpm': getProp('cpm', i / 100), 'zone_id': getProp('zone_id', i + 1), 'size_id': sizeId, 'impression_id': getProp('impression_id', `1-${i}`), 'ad_id': getProp('ad_id', i + 1), 'advertiser': i + 1, 'network': i + 1, 'creative_id': getProp('creative_id', `crid-${i}`), 'type': 'script', 'script': 'alert(\'foo\')', 'campaign_id': i + 1, 'targeting': [ { 'key': getProp('targeting_key', `rpfl_${i}`), 'values': ['43_tier_all_test'] } ] }; } /** * @param {number} i * @param {Array.<Array>} size * @return {{ params: {accountId: string, siteId: string, zoneId: string }, adUnitCode: string, code: string, sizes: *[], bidId: string, bidderRequestId: string }} */ function createBidRequestByIndex(i, size) { return { bidder: 'rubicon', params: { accountId: '14062', siteId: '70608', zoneId: (i + 1).toString(), userId: '12346', position: 'atf', referrer: 'localhost' }, adUnitCode: `/19968336/header-bid-tag-${i}`, code: `div-${i}`, sizes: [size], bidId: i.toString(), bidderRequestId: i.toString(), auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' }; } /** * @param {boolean} [gdprApplies] */ function createGdprBidderRequest(gdprApplies) { if (typeof gdprApplies === 'boolean') { bidderRequest.gdprConsent = { 'consentString': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', 'gdprApplies': gdprApplies }; } else { bidderRequest.gdprConsent = { 'consentString': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' }; } } function createUspBidderRequest() { bidderRequest.uspConsent = '1NYN'; } function createVideoBidderRequest() { createGdprBidderRequest(true); createUspBidderRequest(); let bid = bidderRequest.bids[0]; bid.mediaTypes = { video: { context: 'instream', mimes: ['video/mp4', 'video/x-flv'], api: [2], minduration: 15, playerSize: [640, 480], maxduration: 30, startdelay: 0, playbackmethod: [2], linearity: 1, skip: 1, skipafter: 15, pos: 1, protocols: [1, 2, 3, 4, 5, 6] } }; bid.params.video = { 'language': 'en', 'skip': 1, 'skipafter': 15, 'playerHeight': 480, 'playerWidth': 640, 'size_id': 201, }; bid.userId = { lipb: {lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB']}, idl_env: '1111-2222-3333-4444', tdid: '3000', pubcid: '4000', pubProvidedId: [{ source: 'example.com', uids: [{ id: '333333', ext: { stype: 'ppuid' } }] }, { source: 'id-partner.com', uids: [{ id: '4444444' }] }], criteoId: '1111', }; bid.userIdAsEids = createEidsArray(bid.userId); bid.storedAuctionResponse = 11111; } function createVideoBidderRequestNoVideo() { let bid = bidderRequest.bids[0]; bid.mediaTypes = { video: { context: 'instream' }, }; bid.params.video = ''; } function createVideoBidderRequestOutstream() { let bid = bidderRequest.bids[0]; bid.mediaTypes = { video: { context: 'outstream', mimes: ['video/mp4', 'video/x-flv'], api: [2], minduration: 15, playerSize: [640, 480], maxduration: 30, startdelay: 0, playbackmethod: [2], linearity: 1, skip: 1, skipafter: 15, pos: 1, protocols: [1, 2, 3, 4, 5, 6] }, }; bid.params.accountId = 14062; bid.params.siteId = 70608; bid.params.zoneId = 335918; bid.params.video = { 'language': 'en', 'skip': 1, 'skipafter': 15, 'playerHeight': 320, 'playerWidth': 640, 'size_id': 203 }; } beforeEach(function () { sandbox = sinon.sandbox.create(); logErrorSpy = sinon.spy(utils, 'logError'); getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', bidderRequestId: '178e34bad3658f', bids: [ { bidder: 'rubicon', params: { accountId: '14062', siteId: '70608', zoneId: '335918', pchain: 'GAM:11111-reseller1:22222', userId: '12346', keywords: ['a', 'b', 'c'], inventory: { rating: '5-star', // This actually should not be sent to frank!! causes 400 prodtype: ['tech', 'mobile'] }, visitor: { ucat: 'new', lastsearch: 'iphone', likes: ['sports', 'video games'] }, position: 'atf', referrer: 'localhost', latLong: [40.7607823, '111.8910325'] }, adUnitCode: '/19968336/header-bid-tag-0', code: 'div-1', sizes: [[300, 250], [320, 50]], bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' } ], start: 1472239426002, auctionStart: 1472239426000, timeout: 5000 }; sizeMap = [ {sizeId: 1, size: '468x60'}, {sizeId: 2, size: '728x90'}, {sizeId: 5, size: '120x90'}, {sizeId: 8, size: '120x600'}, {sizeId: 9, size: '160x600'}, {sizeId: 10, size: '300x600'}, {sizeId: 13, size: '200x200'}, {sizeId: 14, size: '250x250'}, {sizeId: 15, size: '300x250'}, {sizeId: 16, size: '336x280'}, {sizeId: 19, size: '300x100'}, {sizeId: 31, size: '980x120'}, {sizeId: 32, size: '250x360'} // Create convenience properties for [sizeAsArray, width, height] by parsing the size string ].map(item => { const sizeAsArray = item.size.split('x').map(s => parseInt(s)); return { sizeId: item.sizeId, size: item.size, sizeAsArray: sizeAsArray.slice(), width: sizeAsArray[0], height: sizeAsArray[1] }; }); }); afterEach(function () { sandbox.restore(); utils.logError.restore(); config.resetConfig(); resetRubiConf(); delete $$PREBID_GLOBAL$$.installedModules; }); describe('MAS mapping / ordering', function () { it('should sort values without any MAS priority sizes in regular ascending order', function () { let ordering = masSizeOrdering([126, 43, 65, 16]); expect(ordering).to.deep.equal([16, 43, 65, 126]); }); it('should sort MAS priority sizes in the proper order w/ rest ascending', function () { let ordering = masSizeOrdering([43, 9, 65, 15, 16, 126]); expect(ordering).to.deep.equal([15, 9, 16, 43, 65, 126]); ordering = masSizeOrdering([43, 15, 9, 65, 16, 126, 2]); expect(ordering).to.deep.equal([15, 2, 9, 16, 43, 65, 126]); ordering = masSizeOrdering([8, 43, 9, 65, 16, 126, 2]); expect(ordering).to.deep.equal([2, 9, 8, 16, 43, 65, 126]); }); }); describe('buildRequests implementation', function () { describe('for requests', function () { describe('to fastlane', function () { it('should make a well-formed request object', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let duplicate = Object.assign(bidderRequest); duplicate.bids[0].params.floor = 0.01; let [request] = spec.buildRequests(duplicate.bids, duplicate); let data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); let expectedQuery = { 'account_id': '14062', 'site_id': '70608', 'zone_id': '335918', 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'x_source.pchain': 'GAM:11111-reseller1:22222', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_i.rating': '5-star', 'tg_i.prodtype': 'tech,mobile', 'tg_fl.eid': 'div-1', 'rf': 'localhost' }; // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { expect(data[key]).to.match(value); } else { expect(data[key]).to.equal(value); } }); }); it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; sinon.spy(bidderRequest.bids[0], 'getFloor'); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); // make sure banner bid called with right stuff expect( bidderRequest.bids[0].getFloor.calledWith({ currency: 'USD', mediaType: 'banner', size: '*' }) ).to.be.true; let data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; // not an object should work and not send getFloorResponse = undefined; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; // make it respond with a non USD floor should not send it getFloorResponse = {currency: 'EUR', floor: 1.0}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; // make it respond with a non USD floor should not send it getFloorResponse = {currency: 'EUR'}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; // make it respond with USD floor and string floor getFloorResponse = {currency: 'USD', floor: '1.23'}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data.rp_hard_floor).to.equal('1.23'); // make it respond with USD floor and num floor getFloorResponse = {currency: 'USD', floor: 1.23}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data.rp_hard_floor).to.equal('1.23'); }); it('should send rp_maxbids to AE if rubicon multibid config exists', function () { var multibidRequest = utils.deepClone(bidderRequest); multibidRequest.bidLimit = 5; let [request] = spec.buildRequests(multibidRequest.bids, multibidRequest); let data = parseQuery(request.data); expect(data['rp_maxbids']).to.equal('5'); }); it('should not send p_pos to AE if not params.position specified', function () { var noposRequest = utils.deepClone(bidderRequest); delete noposRequest.bids[0].params.position; let [request] = spec.buildRequests(noposRequest.bids, noposRequest); let data = parseQuery(request.data); expect(data['site_id']).to.equal('70608'); expect(data['p_pos']).to.equal(undefined); }); it('should not send p_pos to AE if not mediaTypes.banner.pos is invalid', function () { var bidRequest = utils.deepClone(bidderRequest); bidRequest.bids[0].mediaTypes = { banner: { pos: 5 } }; delete bidRequest.bids[0].params.position; let [request] = spec.buildRequests(bidRequest.bids, bidRequest); let data = parseQuery(request.data); expect(data['site_id']).to.equal('70608'); expect(data['p_pos']).to.equal(undefined); }); it('should send p_pos to AE if mediaTypes.banner.pos is valid', function () { var bidRequest = utils.deepClone(bidderRequest); bidRequest.bids[0].mediaTypes = { banner: { pos: 1 } }; delete bidRequest.bids[0].params.position; let [request] = spec.buildRequests(bidRequest.bids, bidRequest); let data = parseQuery(request.data); expect(data['site_id']).to.equal('70608'); expect(data['p_pos']).to.equal('atf'); }); it('should not send p_pos to AE if not params.position is invalid', function () { var badposRequest = utils.deepClone(bidderRequest); badposRequest.bids[0].params.position = 'bad'; let [request] = spec.buildRequests(badposRequest.bids, badposRequest); let data = parseQuery(request.data); expect(data['site_id']).to.equal('70608'); expect(data['p_pos']).to.equal(undefined); }); it('should correctly send p_pos in sra fashion', function() { config.setConfig({rubicon: {singleRequest: true}}); // first one is atf var sraPosRequest = utils.deepClone(bidderRequest); // second is not present const bidCopy = utils.deepClone(sraPosRequest.bids[0]); delete bidCopy.params.position; sraPosRequest.bids.push(bidCopy); // third is btf const bidCopy1 = utils.deepClone(sraPosRequest.bids[0]); bidCopy1.params.position = 'btf'; sraPosRequest.bids.push(bidCopy1); // fourth is invalid (aka not atf or btf) const bidCopy2 = utils.deepClone(sraPosRequest.bids[0]); bidCopy2.params.position = 'unknown'; sraPosRequest.bids.push(bidCopy2); // fifth is not present const bidCopy3 = utils.deepClone(sraPosRequest.bids[0]); delete bidCopy3.params.position; sraPosRequest.bids.push(bidCopy3); let [request] = spec.buildRequests(sraPosRequest.bids, sraPosRequest); let data = parseQuery(request.data); expect(data['p_pos']).to.equal('atf;;btf;;'); }); it('should not send x_source.pchain to AE if params.pchain is not specified', function () { var noPchainRequest = utils.deepClone(bidderRequest); delete noPchainRequest.bids[0].params.pchain; let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); expect(request.data).to.contain('&site_id=70608&'); expect(request.data).to.not.contain('x_source.pchain'); }); it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); }); }); it('should make a well-formed request object without latLong', function () { let expectedQuery = { 'account_id': '14062', 'site_id': '70608', 'zone_id': '335918', 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_i.rating': '5-star', 'tg_i.prodtype': 'tech,mobile', 'rf': 'localhost', 'p_geo.latitude': undefined, 'p_geo.longitude': undefined }; sandbox.stub(Math, 'random').callsFake(() => 0.1); delete bidderRequest.bids[0].params.latLong; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { expect(data[key]).to.match(value); } else { expect(data[key]).to.equal(value); } }); bidderRequest.bids[0].params.latLong = []; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { expect(data[key]).to.match(value); } else { expect(data[key]).to.equal(value); } }); }); it('should add referer info to request data', function () { let refererInfo = { referer: 'https://www.prebid.org', reachedTop: true, numIframes: 1, stack: [ 'https://www.prebid.org/page.html', 'https://www.prebid.org/iframe1.html', ] }; bidderRequest = Object.assign({refererInfo}, bidderRequest); delete bidderRequest.bids[0].params.referrer; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.exist; expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); }); it('page_url should use params.referrer, config.getConfig("pageUrl"), bidderRequest.refererInfo in that order', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('localhost'); delete bidderRequest.bids[0].params.referrer; let refererInfo = {referer: 'https://www.prebid.org'}; bidderRequest = Object.assign({refererInfo}, bidderRequest); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); let origGetConfig = config.getConfig; sandbox.stub(config, 'getConfig').callsFake(function (key) { if (key === 'pageUrl') { return 'https://www.rubiconproject.com'; } return origGetConfig.apply(config, arguments); }); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('https://www.rubiconproject.com'); bidderRequest.bids[0].params.secure = true; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('https://www.rubiconproject.com'); }); it('should use rubicon sizes if present (including non-mappable sizes)', function () { var sizesBidderRequest = utils.deepClone(bidderRequest); sizesBidderRequest.bids[0].params.sizes = [55, 57, 59, 801]; let [request] = spec.buildRequests(sizesBidderRequest.bids, sizesBidderRequest); let data = parseQuery(request.data); expect(data['size_id']).to.equal('55'); expect(data['alt_size_ids']).to.equal('57,59,801'); }); it('should not validate bid request if no valid sizes', function () { var sizesBidderRequest = utils.deepClone(bidderRequest); sizesBidderRequest.bids[0].sizes = [[621, 250], [300, 251]]; let result = spec.isBidRequestValid(sizesBidderRequest.bids[0]); expect(result).to.equal(false); }); it('should not validate bid request if no account id is present', function () { var noAccountBidderRequest = utils.deepClone(bidderRequest); delete noAccountBidderRequest.bids[0].params.accountId; let result = spec.isBidRequestValid(noAccountBidderRequest.bids[0]); expect(result).to.equal(false); }); it('should allow a floor override', function () { var floorBidderRequest = utils.deepClone(bidderRequest); floorBidderRequest.bids[0].params.floor = 2; let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); let data = parseQuery(request.data); expect(data['rp_floor']).to.equal('2'); }); describe('GDPR consent config', function () { it('should send "gdpr" and "gdpr_consent", when gdprConsent defines consentString and gdprApplies', function () { createGdprBidderRequest(true); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['gdpr']).to.equal('1'); expect(data['gdpr_consent']).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); }); it('should send only "gdpr_consent", when gdprConsent defines only consentString', function () { createGdprBidderRequest(); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['gdpr_consent']).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); expect(data['gdpr']).to.equal(undefined); }); it('should not send GDPR params if gdprConsent is not defined', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['gdpr']).to.equal(undefined); expect(data['gdpr_consent']).to.equal(undefined); }); it('should set "gdpr" value as 1 or 0, using "gdprApplies" value of either true/false', function () { createGdprBidderRequest(true); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['gdpr']).to.equal('1'); createGdprBidderRequest(false); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); data = parseQuery(request.data); expect(data['gdpr']).to.equal('0'); }); }); describe('USP Consent', function () { it('should send us_privacy if bidderRequest has a value for uspConsent', function () { createUspBidderRequest(); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['us_privacy']).to.equal('1NYN'); }); it('should not send us_privacy if bidderRequest has no uspConsent value', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); expect(data['us_privacy']).to.equal(undefined); }); }); describe('first party data', function () { it('should not have any tg_v or tg_i params if all are undefined', function () { let params = { inventory: { rating: null, prodtype: undefined }, visitor: { ucat: undefined, lastsearch: null, likes: undefined }, }; // Overwrite the bidder request params with the above ones Object.assign(bidderRequest.bids[0].params, params); // get the built request let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); // make sure that no tg_v or tg_i keys are present in the request let matchingExp = RegExp('^tg_(i|v)\..*$') Object.keys(data).forEach(key => { expect(key).to.not.match(matchingExp); }); }); it('should contain valid params when some are undefined', function () { let params = { inventory: { rating: undefined, prodtype: ['tech', 'mobile'] }, visitor: { ucat: null, lastsearch: 'iphone', likes: undefined }, }; let undefinedKeys = ['tg_i.rating', 'tg_v.ucat', 'tg_v.likes'] let expectedQuery = { 'tg_v.lastsearch': 'iphone', 'tg_i.prodtype': 'tech,mobile', } // Overwrite the bidder request params with the above ones Object.assign(bidderRequest.bids[0].params, params); // get the built request let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); // make sure none of the undefined keys are in query undefinedKeys.forEach(key => { expect(typeof data[key]).to.equal('undefined'); }); // make sure the expected and defined ones do show up still Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; expect(data[key]).to.equal(value); }); }); it('should merge first party data from getConfig with the bid params, if present', () => { const site = { keywords: 'e,f', rating: '4-star', ext: { data: { page: 'home' } }, content: { data: [{ 'name': 'www.dataprovider1.com', 'ext': { 'segtax': 1 }, 'segment': [ { 'id': '987' } ] }, { 'name': 'www.dataprovider1.com', 'ext': { 'segtax': 2 }, 'segment': [ { 'id': '432' } ] }, { 'name': 'www.dataprovider1.com', 'ext': { 'segtax': 5 }, 'segment': [ { 'id': '55' } ] }, { 'name': 'www.dataprovider1.com', 'ext': { 'segtax': 6 }, 'segment': [ { 'id': '66' } ] } ] } }; const user = { data: [{ 'name': 'www.dataprovider1.com', 'ext': { 'segtax': 4 }, 'segment': [ { 'id': '687' }, { 'id': '123' } ] }], gender: 'M', yob: '1984', geo: {country: 'ca'}, keywords: 'd', ext: { data: { age: 40 } } }; sandbox.stub(config, 'getConfig').callsFake(key => { const config = { ortb2: { site, user } }; return utils.deepAccess(config, key); }); const expectedQuery = { 'kw': 'a,b,c,d', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_v.gender': 'M', 'tg_v.age': '40', 'tg_v.iab': '687,123', 'tg_i.iab': '987,432,55,66', 'tg_v.yob': '1984', 'tg_i.rating': '4-star,5-star', 'tg_i.page': 'home', 'tg_i.prodtype': 'tech,mobile', }; // get the built request let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = parseQuery(request.data); // make sure that tg_v, tg_i, and kw values are correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; expect(data[key]).to.deep.equal(value); }); }); }); describe('singleRequest config', function () { it('should group all bid requests with the same site id', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); config.setConfig({rubicon: {singleRequest: true}}); const expectedQuery = { 'account_id': '14062', 'site_id': '70608', 'zone_id': '335918', 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_i.rating': '5-star', 'tg_i.prodtype': 'tech,mobile', 'tg_fl.eid': 'div-1', 'rf': 'localhost' }; const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidCopy.params.siteId = '70608'; bidCopy.params.zoneId = '1111'; bidderRequest.bids.push(bidCopy); const bidCopy2 = utils.deepClone(bidderRequest.bids[0]); bidCopy2.params.siteId = '99999'; bidCopy2.params.zoneId = '2222'; bidderRequest.bids.push(bidCopy2); const bidCopy3 = utils.deepClone(bidderRequest.bids[0]); bidCopy3.params.siteId = '99999'; bidCopy3.params.zoneId = '3333'; bidderRequest.bids.push(bidCopy3); const serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); // array length should match the num of unique 'siteIds' expect(serverRequests).to.be.a('array'); expect(serverRequests).to.have.lengthOf(2); // collect all bidRequests so order can be checked against the url param slot order const bidRequests = serverRequests.reduce((aggregator, item) => aggregator.concat(item.bidRequest), []); let bidRequestIndex = 0; serverRequests.forEach(item => { expect(item).to.be.a('object'); expect(item).to.have.property('method'); expect(item).to.have.property('url'); expect(item).to.have.property('data'); expect(item).to.have.property('bidRequest'); expect(item.method).to.equal('GET'); expect(item.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); expect(item.data).to.be.a('string'); // 'bidRequest' type must be 'array' if SRA enabled expect(item.bidRequest).to.be.a('array').to.have.lengthOf(2); item.bidRequest.forEach((bidRequestItem, i, array) => { expect(bidRequestItem).to.be.a('object'); // every 'siteId' values need to match expect(bidRequestItem.params.siteId).to.equal(array[0].params.siteId); }); const data = parseQuery(item.data); Object.keys(expectedQuery).forEach(key => { expect(data).to.have.property(key); // extract semicolon delineated values const params = data[key].split(';'); // skip value test for site and zone ids if (key !== 'site_id' && key !== 'zone_id') { if (expectedQuery[key] instanceof RegExp) { params.forEach(paramItem => { expect(paramItem).to.match(expectedQuery[key]); }); } else { expect(params).to.contain(expectedQuery[key]); } } // check parsed url data list order with requestBid list, items must have same index in both lists if (key === 'zone_id') { params.forEach((p) => { expect(bidRequests[bidRequestIndex]).to.be.a('object'); expect(bidRequests[bidRequestIndex].params).to.be.a('object'); // 'zone_id' is used to verify so each bid must have a unique 'zone_id' expect(p).to.equal(bidRequests[bidRequestIndex].params.zoneId); // increment to next bidRequest index having verified that item positions match in url params and bidRequest lists bidRequestIndex++; }); } }); }); }); it('should not send more than 10 bids in a request (split into separate requests with <= 10 bids each)', function () { config.setConfig({rubicon: {singleRequest: true}}); let serverRequests; let data; // TEST '10' BIDS, add 9 to 1 existing bid for (let i = 0; i < 9; i++) { let bidCopy = utils.deepClone(bidderRequest.bids[0]); bidCopy.params.zoneId = `${i}0000`; bidderRequest.bids.push(bidCopy); } serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); // '10' bids per SRA request: so there should be 1 request expect(serverRequests.length).to.equal(1); // and that one request should have data from 10 bids expect(serverRequests[0].bidRequest).to.have.lengthOf(10); // check that slots param value matches expect(serverRequests[0].data.indexOf('&slots=10&') !== -1).to.equal(true); // check that zone_id has 10 values (since all zone_ids are unique all should exist in get param) data = parseQuery(serverRequests[0].data); expect(data).to.be.a('object'); expect(data).to.have.property('zone_id'); expect(data.zone_id.split(';')).to.have.lengthOf(10); // TEST '100' BIDS, add 90 to the previously added 10 for (let i = 0; i < 90; i++) { let bidCopy = utils.deepClone(bidderRequest.bids[0]); bidCopy.params.zoneId = `${(i + 10)}0000`; bidderRequest.bids.push(bidCopy); } serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); // '100' bids: should be '10' SRA requests expect(serverRequests.length).to.equal(10); // check that each request has 10 items serverRequests.forEach((serverRequest) => { // and that one request should have data from 10 bids expect(serverRequest.bidRequest).to.have.lengthOf(10); // check that slots param value matches expect(serverRequest.data.indexOf('&slots=10&') !== -1).to.equal(true); }); }); it('should not group bid requests if singleRequest does not equal true', function () { config.setConfig({rubicon: {singleRequest: false}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); const bidCopy2 = utils.deepClone(bidderRequest.bids[0]); bidCopy2.params.siteId = '32001'; bidderRequest.bids.push(bidCopy2); const bidCopy3 = utils.deepClone(bidderRequest.bids[0]); bidCopy3.params.siteId = '32001'; bidderRequest.bids.push(bidCopy3); let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(serverRequests).that.is.an('array').of.length(4); }); it('should not group video bid requests', function () { config.setConfig({rubicon: {singleRequest: true}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); const bidCopy2 = utils.deepClone(bidderRequest.bids[0]); bidCopy2.params.siteId = '32001'; bidderRequest.bids.push(bidCopy2); const bidCopy3 = utils.deepClone(bidderRequest.bids[0]); bidCopy3.params.siteId = '32001'; bidderRequest.bids.push(bidCopy3); const bidCopy4 = utils.deepClone(bidderRequest.bids[0]); bidCopy4.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], mimes: ['video/mp4', 'video/x-ms-wmv'], protocols: [2, 5], maxduration: 30, linearity: 1, api: [2] } }; bidCopy4.params.video = { 'language': 'en', 'p_aso.video.ext.skip': true, 'p_aso.video.ext.skipdelay': 15, 'playerHeight': 320, 'playerWidth': 640, 'size_id': 201, 'aeParams': { 'p_aso.video.ext.skip': '1', 'p_aso.video.ext.skipdelay': '15' } }; bidderRequest.bids.push(bidCopy4); let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(serverRequests).that.is.an('array').of.length(3); }); }); describe('user id config', function () { it('should send tpid_tdid when userIdAsEids contains unifiedId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { tdid: 'abcd-efgh-ijkl-mnop-1234' }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['tpid_tdid']).to.equal('abcd-efgh-ijkl-mnop-1234'); expect(data['eid_adserver.org']).to.equal('abcd-efgh-ijkl-mnop-1234'); }); describe('LiveIntent support', function () { it('should send tpid_liveintent.com when userIdAsEids contains liveintentId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB'] } }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['tpid_liveintent.com']).to.equal('0000-1111-2222-3333'); expect(data['eid_liveintent.com']).to.equal('0000-1111-2222-3333'); expect(data['tg_v.LIseg']).to.equal('segA,segB'); }); it('should send tg_v.LIseg when userIdAsEids contains liveintentId with ext.segments as array', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { lipbid: '1111-2222-3333-4444', segments: ['segD', 'segE'] } }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); const unescapedData = unescape(request.data); expect(unescapedData.indexOf('&tpid_liveintent.com=1111-2222-3333-4444&') !== -1).to.equal(true); expect(unescapedData.indexOf('&tg_v.LIseg=segD,segE&') !== -1).to.equal(true); }); }); describe('LiveRamp support', function () { it('should send x_liverampidl when userIdAsEids contains liverampId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { idl_env: '1111-2222-3333-4444' }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); }); }); describe('pubcid support', function () { it('should send eid_pubcid.org when userIdAsEids contains pubcid', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { pubcid: '1111' }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['eid_pubcid.org']).to.equal('1111^1'); }); }); describe('Criteo support', function () { it('should send eid_criteo.com when userIdAsEids contains criteo', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { criteoId: '1111' }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['eid_criteo.com']).to.equal('1111^1'); }); }); describe('pubProvidedId support', function () { it('should send pubProvidedId when userIdAsEids contains pubProvidedId ids', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { pubProvidedId: [{ source: 'example.com', uids: [{ id: '11111', ext: { stype: 'ppuid' } }] }, { source: 'id-partner.com', uids: [{ id: '222222' }] }] }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['ppuid']).to.equal('11111'); }); }); describe('ID5 support', function () { it('should send ID5 id when userIdAsEids contains ID5', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { id5id: { uid: '11111', ext: { linkType: '22222' } } }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['eid_id5-sync.com']).to.equal('11111^1^22222'); }); }); describe('UserID catchall support', function () { it('should send user id with generic format', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js clonedBid.userIdAsEids = [{ source: 'catchall', uids: [{ id: '11111', atype: 2 }] }] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['eid_catchall']).to.equal('11111^2');