mk9-prebid
Version:
Header Bidding Management Library
1,334 lines (1,163 loc) • 132 kB
JavaScript
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');