mk9-prebid
Version:
Header Bidding Management Library
1,445 lines (1,357 loc) • 57.6 kB
JavaScript
import {expect} from 'chai';
import * as utils from 'src/utils.js';
import { getGlobal } from 'src/prebidGlobal.js';
import CONSTANTS from 'src/constants.json';
import {
_floorDataForAuction,
getFloorsDataForAuction,
getFirstMatchingFloor,
getFloor,
handleSetFloorsConfig,
requestBidsHook,
isFloorsDataValid,
addBidResponseHook,
fieldMatchingFunctions,
allowedFields
} from 'modules/priceFloors.js';
import events from 'src/events.js';
describe('the price floors module', function () {
let logErrorSpy;
let logWarnSpy;
let sandbox;
let clock;
const basicFloorData = {
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
currency: 'USD',
schema: {
delimiter: '|',
fields: ['mediaType']
},
values: {
'banner': 1.0,
'video': 5.0,
'*': 2.5
}
};
const basicFloorDataHigh = {
floorMin: 7.0,
modelVersion: 'basic model',
modelWeight: 10,
currency: 'USD',
schema: {
delimiter: '|',
fields: ['mediaType']
},
values: {
'banner': 1.0,
'video': 5.0,
'*': 2.5
}
};
const basicFloorDataLow = {
floorMin: 2.3,
modelVersion: 'basic model',
modelWeight: 10,
currency: 'USD',
schema: {
delimiter: '|',
fields: ['mediaType']
},
values: {
'banner': 1.0,
'video': 5.0,
'*': 2.5
}
};
const basicFloorConfig = {
enabled: true,
auctionDelay: 0,
endpoint: {},
enforcement: {
enforceJS: true,
enforcePBS: false,
floorDeals: false,
bidAdjustment: true
},
data: basicFloorData
}
const minFloorConfigHigh = {
enabled: true,
auctionDelay: 0,
floorMin: 7,
endpoint: {},
enforcement: {
enforceJS: true,
enforcePBS: false,
floorDeals: false,
bidAdjustment: true
},
data: basicFloorDataHigh
}
const minFloorConfigLow = {
enabled: true,
auctionDelay: 0,
floorMin: 2.3,
endpoint: {},
enforcement: {
enforceJS: true,
enforcePBS: false,
floorDeals: false,
bidAdjustment: true
},
data: basicFloorDataLow
}
const basicBidRequest = {
bidder: 'rubicon',
adUnitCode: 'test_div_1',
auctionId: '1234-56-789',
};
function getAdUnitMock(code = 'adUnit-code') {
return {
code,
mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}},
bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}]
};
}
beforeEach(function() {
clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create();
logErrorSpy = sinon.spy(utils, 'logError');
logWarnSpy = sinon.spy(utils, 'logWarn');
});
afterEach(function() {
clock.restore();
handleSetFloorsConfig({enabled: false});
sandbox.restore();
utils.logError.restore();
utils.logWarn.restore();
// reset global bidder settings so no weird test side effects
getGlobal().bidderSettings = {};
});
describe('getFloorsDataForAuction', function () {
it('converts basic input floor data into a floorData map for the auction correctly', function () {
// basic input where nothing needs to be updated
expect(getFloorsDataForAuction(basicFloorData)).to.deep.equal(basicFloorData);
// if cur and delim not defined then default to correct ones (usd and |)
let inputFloorData = utils.deepClone(basicFloorData);
delete inputFloorData.currency;
delete inputFloorData.schema.delimiter;
expect(getFloorsDataForAuction(inputFloorData)).to.deep.equal(basicFloorData);
// should not use defaults if differing values
inputFloorData.currency = 'EUR'
inputFloorData.schema.delimiter = '^'
let resultingData = getFloorsDataForAuction(inputFloorData);
expect(resultingData.currency).to.equal('EUR');
expect(resultingData.schema.delimiter).to.equal('^');
});
it('converts more complex floor data correctly', function () {
let inputFloorData = {
schema: {
fields: ['mediaType', 'size', 'domain']
},
values: {
'banner|300x250|prebid.org': 1.0,
'video|640x480|prebid.org': 5.0,
'banner|728x90|rubicon.com': 3.5,
'video|600x300|appnexus.com': 3.5,
'*|*|prebid.org': 3.5,
}
};
let resultingData = getFloorsDataForAuction(inputFloorData);
expect(resultingData).to.deep.equal({
currency: 'USD',
schema: {
delimiter: '|',
fields: ['mediaType', 'size', 'domain']
},
values: {
'banner|300x250|prebid.org': 1.0,
'video|640x480|prebid.org': 5.0,
'banner|728x90|rubicon.com': 3.5,
'video|600x300|appnexus.com': 3.5,
'*|*|prebid.org': 3.5,
}
});
});
it('adds adUnitCode to the schema if the floorData comes from adUnit level to maintain scope', function () {
let inputFloorData = utils.deepClone(basicFloorData);
let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1');
expect(resultingData).to.deep.equal({
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
currency: 'USD',
schema: {
delimiter: '|',
fields: ['adUnitCode', 'mediaType']
},
values: {
'test_div_1|banner': 1.0,
'test_div_1|video': 5.0,
'test_div_1|*': 2.5
}
});
// uses the right delim if not |
inputFloorData.schema.delimiter = '^';
resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div');
expect(resultingData).to.deep.equal({
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
currency: 'USD',
schema: {
delimiter: '^',
fields: ['adUnitCode', 'mediaType']
},
values: {
'this_is_a_div^banner': 1.0,
'this_is_a_div^video': 5.0,
'this_is_a_div^*': 2.5
}
});
});
});
describe('getFirstMatchingFloor', function () {
it('selects the right floor for different mediaTypes', function () {
// banner with * size (not in rule file so does not do anything)
expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
floorMin: 0,
floorRuleValue: 1.0,
matchingFloor: 1.0,
matchingData: 'banner',
matchingRule: 'banner'
});
// video with * size (not in rule file so does not do anything)
expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({
floorMin: 0,
floorRuleValue: 5.0,
matchingFloor: 5.0,
matchingData: 'video',
matchingRule: 'video'
});
// native (not in the rule list) with * size (not in rule file so does not do anything)
expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({
floorMin: 0,
floorRuleValue: 2.5,
matchingFloor: 2.5,
matchingData: 'native',
matchingRule: '*'
});
// banner with floorMin higher than matching rule
handleSetFloorsConfig({
...minFloorConfigHigh
});
expect(getFirstMatchingFloor({...basicFloorDataHigh}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
floorMin: 7,
floorRuleValue: 1.0,
matchingFloor: 7,
matchingData: 'banner',
matchingRule: 'banner'
});
// banner with floorMin higher than matching rule
handleSetFloorsConfig({
...minFloorConfigLow
});
expect(getFirstMatchingFloor({...basicFloorDataLow}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({
floorMin: 2.3,
floorRuleValue: 5,
matchingFloor: 5,
matchingData: 'video',
matchingRule: 'video'
});
});
it('does not alter cached matched input if conversion occurs', function () {
let inputData = {...basicFloorData};
[0.2, 0.4, 0.6, 0.8].forEach(modifier => {
let result = getFirstMatchingFloor(inputData, basicBidRequest, {mediaType: 'banner', size: '*'});
// result should always be the same
expect(result).to.deep.equal({
floorMin: 0,
floorRuleValue: 1.0,
matchingFloor: 1.0,
matchingData: 'banner',
matchingRule: 'banner'
});
// make sure a post retrieval adjustment does not alter the cached floor
result.matchingFloor = result.matchingFloor * modifier;
});
});
it('selects the right floor for different sizes', function () {
let inputFloorData = {
currency: 'USD',
schema: {
delimiter: '|',
fields: ['size']
},
values: {
'300x250': 1.1,
'640x480': 2.2,
'728x90': 3.3,
'600x300': 4.4,
'*': 5.5,
}
}
// banner with 300x250 size
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 1.1,
matchingFloor: 1.1,
matchingData: '300x250',
matchingRule: '300x250'
});
// video with 300x250 size
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 1.1,
matchingFloor: 1.1,
matchingData: '300x250',
matchingRule: '300x250'
});
// native (not in the rule list) with 300x600 size
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 4.4,
matchingFloor: 4.4,
matchingData: '600x300',
matchingRule: '600x300'
});
// n/a mediaType with a size not in file should go to catch all
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 5.5,
matchingFloor: 5.5,
matchingData: '1x1',
matchingRule: '*'
});
});
it('selects the right floor for more complex rules', function () {
let inputFloorData = {
currency: 'USD',
schema: {
delimiter: '^',
fields: ['adUnitCode', 'mediaType', 'size']
},
values: {
'test_div_1^banner^300x250': 1.1,
'test_div_1^video^640x480': 2.2,
'test_div_2^*^*': 3.3,
'*^banner^300x250': 4.4,
'weird_div^*^300x250': 5.5
},
default: 0.5
};
// banner with 300x250 size
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 1.1,
matchingFloor: 1.1,
matchingData: 'test_div_1^banner^300x250',
matchingRule: 'test_div_1^banner^300x250'
});
// video with 300x250 size -> No matching rule so should use default
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 0.5,
matchingFloor: 0.5,
matchingData: 'test_div_1^video^300x250',
matchingRule: undefined
});
// remove default and should still return the same floor as above since matches are cached
delete inputFloorData.default;
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 0.5,
matchingFloor: 0.5,
matchingData: 'test_div_1^video^300x250',
matchingRule: undefined
});
// update adUnitCode to test_div_2 with weird other params
let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' }
expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({
floorMin: 0,
floorRuleValue: 3.3,
matchingFloor: 3.3,
matchingData: 'test_div_2^badmediatype^900x900',
matchingRule: 'test_div_2^*^*'
});
});
it('it does not break if floorData has bad values', function () {
let inputFloorData = {};
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
matchingFloor: undefined
});
// if default is there use it
inputFloorData = { default: 5.0 };
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
matchingFloor: 5.0
});
});
});
describe('pre-auction tests', function () {
let exposedAdUnits;
const validateBidRequests = (getFloorExpected, FloorDataExpected) => {
exposedAdUnits.forEach(adUnit => adUnit.bids.forEach(bid => {
expect(bid.hasOwnProperty('getFloor')).to.equal(getFloorExpected);
expect(bid.floorData).to.deep.equal(FloorDataExpected);
}));
};
const runStandardAuction = (adUnits = [getAdUnitMock('test_div_1')]) => {
requestBidsHook(config => exposedAdUnits = config.adUnits, {
auctionId: basicBidRequest.auctionId,
adUnits,
});
};
let fakeFloorProvider;
let actualAllowedFields = allowedFields;
let actualFieldMatchingFunctions = fieldMatchingFunctions;
const defaultAllowedFields = [...allowedFields];
const defaultMatchingFunctions = {...fieldMatchingFunctions};
beforeEach(function() {
fakeFloorProvider = sinon.fakeServer.create();
});
afterEach(function() {
fakeFloorProvider.restore();
exposedAdUnits = undefined;
actualAllowedFields = [...defaultAllowedFields];
actualFieldMatchingFunctions = {...defaultMatchingFunctions};
});
it('should not do floor stuff if no resulting floor object can be resolved for auciton', function () {
handleSetFloorsConfig({
...basicFloorConfig,
data: undefined
});
runStandardAuction();
validateBidRequests(false, {
skipped: true,
floorMin: undefined,
modelVersion: undefined,
modelWeight: undefined,
modelTimestamp: undefined,
location: 'noData',
skipRate: 0,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('should use adUnit level data if not setConfig or fetch has occured', function () {
handleSetFloorsConfig({
...basicFloorConfig,
data: undefined
});
// attach floor data onto an adUnit and run an auction
let adUnitWithFloors1 = {
...getAdUnitMock('adUnit-Div-1'),
floors: {
...basicFloorData,
modelVersion: 'adUnit Model Version', // change the model name
}
};
let adUnitWithFloors2 = {
...getAdUnitMock('adUnit-Div-2'),
floors: {
...basicFloorData,
values: {
'banner': 5.0,
'*': 10.4
}
}
};
runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]);
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'adUnit Model Version',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'adUnit',
skipRate: 0,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('should use adUnit level data and minFloor should be set', function () {
handleSetFloorsConfig({
...minFloorConfigHigh,
data: undefined
});
// attach floor data onto an adUnit and run an auction
let adUnitWithFloors1 = {
...getAdUnitMock('adUnit-Div-1'),
floors: {
...basicFloorData,
modelVersion: 'adUnit Model Version', // change the model name
}
};
let adUnitWithFloors2 = {
...getAdUnitMock('adUnit-Div-2'),
floors: {
...basicFloorData,
values: {
'banner': 5.0,
'*': 10.4
}
}
};
runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]);
validateBidRequests(true, {
skipped: false,
modelVersion: 'adUnit Model Version',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'adUnit',
skipRate: 0,
floorMin: 7,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () {
handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'});
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'floorprovider'
});
});
it('should pick the right floorProvider', function () {
let inputFloors = {
...basicFloorConfig,
floorProvider: 'providerA',
data: {
...basicFloorData,
floorProvider: 'providerB',
}
};
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'providerB'
});
// if not at data level take top level
delete inputFloors.data.floorProvider;
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'providerA'
});
// if none should be undefined
delete inputFloors.floorProvider;
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('should take the right skipRate depending on input', function () {
// first priority is data object
sandbox.stub(Math, 'random').callsFake(() => 0.99);
let inputFloors = {
...basicFloorConfig,
skipRate: 10,
data: {
...basicFloorData,
skipRate: 50
}
};
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 50,
fetchStatus: undefined,
floorProvider: undefined
});
// if that does not exist uses topLevel skipRate setting
delete inputFloors.data.skipRate;
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 10,
fetchStatus: undefined,
floorProvider: undefined
});
// if that is not there defaults to zero
delete inputFloors.skipRate;
handleSetFloorsConfig(inputFloors);
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('should randomly pick a model if floorsSchemaVersion is 2', function () {
let inputFloors = {
...basicFloorConfig,
floorProvider: 'floorprovider',
data: {
floorsSchemaVersion: 2,
currency: 'USD',
modelGroups: [
{
modelVersion: 'model-1',
modelWeight: 10,
schema: {
delimiter: '|',
fields: ['mediaType']
},
values: {
'banner': 1.0,
'*': 2.5
}
}, {
modelVersion: 'model-2',
modelWeight: 40,
schema: {
delimiter: '|',
fields: ['size']
},
values: {
'300x250': 1.0,
'*': 2.5
}
}, {
modelVersion: 'model-3',
modelWeight: 50,
schema: {
delimiter: '|',
fields: ['domain']
},
values: {
'www.prebid.org': 1.0,
'*': 2.5
}
}
]
}
};
handleSetFloorsConfig(inputFloors);
// stub random to give us wanted vals
let randValue;
sandbox.stub(Math, 'random').callsFake(() => randValue);
// 0 - 10 should use first model
randValue = 0.05;
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'model-1',
modelWeight: 10,
modelTimestamp: undefined,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'floorprovider'
});
// 11 - 50 should use second model
randValue = 0.40;
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'model-2',
modelWeight: 40,
modelTimestamp: undefined,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'floorprovider'
});
// 51 - 100 should use third model
randValue = 0.75;
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'model-3',
modelWeight: 50,
modelTimestamp: undefined,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: 'floorprovider'
});
});
it('should not overwrite previous data object if the new one is bad', function () {
handleSetFloorsConfig({...basicFloorConfig});
handleSetFloorsConfig({
...basicFloorConfig,
data: undefined
});
handleSetFloorsConfig({
...basicFloorConfig,
data: 5
});
handleSetFloorsConfig({
...basicFloorConfig,
data: {
schema: {fields: ['thisIsNotAllowedSoShouldFail']},
values: {'*': 1.2},
modelVersion: 'FAIL'
}
});
runStandardAuction();
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: undefined,
floorProvider: undefined
});
});
it('should dynamically add new schema fileds and functions if added via setConfig', function () {
let deviceSpoof;
handleSetFloorsConfig({
...basicFloorConfig,
data: {
schema: {fields: ['deviceType']},
values: {
'mobile': 1.0,
'desktop': 2.0,
'tablet': 3.0,
'*': 4.0
}
},
additionalSchemaFields: {
deviceType: () => deviceSpoof
}
});
expect(allowedFields).to.contain('deviceType');
expect(fieldMatchingFunctions['deviceType']).to.be.a('function');
// run getFloor to make sure it selcts right stuff! (other params do not matter since we only are testing deviceType)
runStandardAuction();
// set deviceType to mobile;
deviceSpoof = 'mobile';
exposedAdUnits[0].bids[0].auctionId = basicBidRequest.auctionId
expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({
currency: 'USD',
floor: 1.0 // 'mobile': 1.0,
});
// set deviceType to desktop;
deviceSpoof = 'desktop';
expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({
currency: 'USD',
floor: 2.0 // 'desktop': 2.0,
});
// set deviceType to tablet;
deviceSpoof = 'tablet';
expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({
currency: 'USD',
floor: 3.0 // 'tablet': 3.0
});
// set deviceType to unknown;
deviceSpoof = 'unknown';
expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({
currency: 'USD',
floor: 4.0 // '*': 4.0
});
});
it('Should continue auction of delay is hit without a response from floor provider', function () {
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// start the auction it should delay and not immediately call `continueAuction`
runStandardAuction();
// exposedAdUnits should be undefined if the auction has not continued
expect(exposedAdUnits).to.be.undefined;
// hit the delay
clock.tick(250);
// log warn should be called and adUnits not undefined
expect(logWarnSpy.calledOnce).to.equal(true);
expect(exposedAdUnits).to.not.be.undefined;
// the exposedAdUnits should be from the fetch not setConfig level data
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: 'timeout',
floorProvider: undefined
});
fakeFloorProvider.respond();
});
it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () {
// init the fake server with response stuff
let fetchFloorData = {
...basicFloorData,
modelVersion: 'fetch model name', // change the model name
};
fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData));
// run setConfig indicating fetch
handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// floor provider should be called
expect(fakeFloorProvider.requests.length).to.equal(1);
expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json');
// start the auction it should delay and not immediately call `continueAuction`
runStandardAuction();
// exposedAdUnits should be undefined if the auction has not continued
expect(exposedAdUnits).to.be.undefined;
// make the fetch respond
fakeFloorProvider.respond();
expect(exposedAdUnits).to.not.be.undefined;
// the exposedAdUnits should be from the fetch not setConfig level data
// and fetchStatus is success since fetch worked
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'fetch model name',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'fetch',
skipRate: 0,
fetchStatus: 'success',
floorProvider: 'floorprovider'
});
});
it('it should correctly overwrite floorProvider with fetch provider', function () {
// init the fake server with response stuff
let fetchFloorData = {
...basicFloorData,
floorProvider: 'floorProviderD', // change the floor provider
modelVersion: 'fetch model name', // change the model name
};
fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData));
// run setConfig indicating fetch
handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// floor provider should be called
expect(fakeFloorProvider.requests.length).to.equal(1);
expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json');
// start the auction it should delay and not immediately call `continueAuction`
runStandardAuction();
// exposedAdUnits should be undefined if the auction has not continued
expect(exposedAdUnits).to.be.undefined;
// make the fetch respond
fakeFloorProvider.respond();
// the exposedAdUnits should be from the fetch not setConfig level data
// and fetchStatus is success since fetch worked
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'fetch model name',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'fetch',
skipRate: 0,
fetchStatus: 'success',
floorProvider: 'floorProviderD'
});
});
it('it should correctly overwrite skipRate with fetch skipRate', function () {
// so floors does not skip
sandbox.stub(Math, 'random').callsFake(() => 0.99);
// init the fake server with response stuff
let fetchFloorData = {
...basicFloorData,
modelVersion: 'fetch model name', // change the model name
};
fetchFloorData.skipRate = 95;
fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData));
// run setConfig indicating fetch
handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// floor provider should be called
expect(fakeFloorProvider.requests.length).to.equal(1);
expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json');
// start the auction it should delay and not immediately call `continueAuction`
runStandardAuction();
// exposedAdUnits should be undefined if the auction has not continued
expect(exposedAdUnits).to.be.undefined;
// make the fetch respond
fakeFloorProvider.respond();
expect(exposedAdUnits).to.not.be.undefined;
// the exposedAdUnits should be from the fetch not setConfig level data
// and fetchStatus is success since fetch worked
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'fetch model name',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'fetch',
skipRate: 95,
fetchStatus: 'success',
floorProvider: 'floorprovider'
});
});
it('Should not break if floor provider returns 404', function () {
// run setConfig indicating fetch
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// run the auction and make server respond with 404
fakeFloorProvider.respond();
runStandardAuction();
// error should have been called for fetch error
expect(logErrorSpy.calledOnce).to.equal(true);
// should have caught the response error and still used setConfig data
// and fetch failed is true
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: 'error',
floorProvider: undefined
});
});
it('Should not break if floor provider returns non json', function () {
fakeFloorProvider.respondWith('Not valid response');
// run setConfig indicating fetch
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// run the auction and make server respond
fakeFloorProvider.respond();
runStandardAuction();
// error should have been called for response floor data not being valid
expect(logErrorSpy.calledOnce).to.equal(true);
// should have caught the response error and still used setConfig data
// and fetchStatus is 'success' but location is setConfig since it had bad data
validateBidRequests(true, {
skipped: false,
floorMin: undefined,
modelVersion: 'basic model',
modelWeight: 10,
modelTimestamp: 1606772895,
location: 'setConfig',
skipRate: 0,
fetchStatus: 'success',
floorProvider: undefined
});
});
it('should handle not using fetch correctly', function () {
// run setConfig twice indicating fetch
fakeFloorProvider.respondWith(JSON.stringify(basicFloorData));
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
// log warn should be called and server only should have one request
expect(logWarnSpy.calledOnce).to.equal(true);
expect(fakeFloorProvider.requests.length).to.equal(1);
expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json');
// now we respond and then run again it should work and make another request
fakeFloorProvider.respond();
handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}});
fakeFloorProvider.respond();
// now warn still only called once and server called twice
expect(logWarnSpy.calledOnce).to.equal(true);
expect(fakeFloorProvider.requests.length).to.equal(2);
// should log error if method is not GET for now
expect(logErrorSpy.calledOnce).to.equal(false);
handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}});
expect(logErrorSpy.calledOnce).to.equal(true);
});
describe('isFloorsDataValid', function () {
it('should return false if unknown floorsSchemaVersion', function () {
let inputFloorData = utils.deepClone(basicFloorData);
inputFloorData.floorsSchemaVersion = 3;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
});
it('should work correctly for fields array', function () {
let inputFloorData = utils.deepClone(basicFloorData);
expect(isFloorsDataValid(inputFloorData)).to.to.equal(true);
// no fields array
delete inputFloorData.schema.fields;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
// Fields is not an array
inputFloorData.schema.fields = {};
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.schema.fields = undefined;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.schema.fields = 'adUnitCode';
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
// fields has a value that is not one of the "allowed" fields
inputFloorData.schema.fields = ['adUnitCode', 'notValidMapping'];
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
});
it('should work correctly for values object', function () {
let inputFloorData = utils.deepClone(basicFloorData);
expect(isFloorsDataValid(inputFloorData)).to.to.equal(true);
// no values object
delete inputFloorData.values;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
// values is not correct type
inputFloorData.values = [];
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.values = '123455/slot';
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
// is an object but structure is wrong
inputFloorData.values = {
'banner': 'not a floor value'
};
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.values = {
'banner': undefined
};
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
// should be true if at least one rule is valid
inputFloorData.schema.fields = ['adUnitCode', 'mediaType'];
inputFloorData.values = {
'banner': 1.0,
'test-div-1|native': 1.0, // only valid rule should still work and delete the other rules
'video': 1.0,
'*': 1.0
};
expect(isFloorsDataValid(inputFloorData)).to.to.equal(true);
expect(inputFloorData.values).to.deep.equal({ 'test-div-1|native': 1.0 });
});
it('should work correctly for floorsSchemaVersion 2', function () {
let inputFloorData = {
floorsSchemaVersion: 2,
currency: 'USD',
modelGroups: [
{
modelVersion: 'model-1',
modelWeight: 10,
schema: {
delimiter: '|',
fields: ['mediaType']
},
values: {
'banner': 1.0,
'*': 2.5
}
}, {
modelVersion: 'model-2',
modelWeight: 40,
schema: {
delimiter: '|',
fields: ['size']
},
values: {
'300x250': 1.0,
'*': 2.5
}
}, {
modelVersion: 'model-3',
modelWeight: 50,
schema: {
delimiter: '|',
fields: ['domain']
},
values: {
'www.prebid.org': 1.0,
'*': 2.5
}
}
]
};
expect(isFloorsDataValid(inputFloorData)).to.to.equal(true);
// remove one of the modelWeight's and it should be false
delete inputFloorData.modelGroups[1].modelWeight;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.modelGroups[1].modelWeight = 40;
// remove values from a model and it should not validate
const tempValues = {...inputFloorData.modelGroups[0].values};
delete inputFloorData.modelGroups[0].values;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.modelGroups[0].values = tempValues;
// modelGroups should be an array and have at least one entry
delete inputFloorData.modelGroups;
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
inputFloorData.modelGroups = [];
expect(isFloorsDataValid(inputFloorData)).to.to.equal(false);
});
});
describe('getFloor', function () {
let bidRequest = {
...basicBidRequest,
getFloor
};
it('returns empty if no matching data for auction is found', function () {
expect(bidRequest.getFloor({})).to.deep.equal({});
});
it('picks the right rule depending on input', function () {
_floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig);
// empty params into getFloor should use default of banner * FloorData Curr
let inputParams = {};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 1.0
});
// ask for banner
inputParams = {mediaType: 'banner'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 1.0
});
// ask for video
inputParams = {mediaType: 'video'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 5.0
});
// ask for *
inputParams = {mediaType: '*'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 2.5
});
});
it('picks the right rule with more complex rules', function () {
_floorDataForAuction[bidRequest.auctionId] = {
...basicFloorConfig,
data: {
currency: 'USD',
schema: { fields: ['mediaType', 'size'], delimiter: '|' },
values: {
'banner|300x250': 0.5,
'banner|300x600': 1.5,
'banner|728x90': 2.5,
'banner|*': 3.5,
'video|640x480': 4.5,
'video|*': 5.5
},
default: 10.0
}
};
// assumes banner *
let inputParams = {};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 3.5
});
// ask for banner with a size
inputParams = {mediaType: 'banner', size: [300, 600]};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 1.5
});
// ask for video with a size
inputParams = {mediaType: 'video', size: [640, 480]};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 4.5
});
// ask for video with a size not in rules (should pick rule which has video and *)
inputParams = {mediaType: 'video', size: [111, 222]};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 5.5
});
// ask for native * but no native rule so should use default value if there
inputParams = {mediaType: 'native', size: '*'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 10.0
});
});
it('should round up to 4 decimal places', function () {
_floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig);
_floorDataForAuction[bidRequest.auctionId].data.values = {
'banner': 1.777777,
'video': 1.1111111,
};
// assumes banner *
let inputParams = {mediaType: 'banner'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 1.7778
});
// assumes banner *
inputParams = {mediaType: 'video'};
expect(bidRequest.getFloor(inputParams)).to.deep.equal({
currency: 'USD',
floor: 1.1112
});
});
it('should return the adjusted floor if bidder has cpm adjustment function', function () {
getGlobal().bidderSettings = {
rubicon: {
bidCpmAdjustment: function (bidCpm) {
return bidCpm * 0.5;
},
},
appnexus: {
bidCpmAdjustment: function (bidCpm) {
return bidCpm * 0.75;
},
}
};
_floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig);
_floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 };
let appnexusBid = {
...bidRequest,
bidder: 'appnexus'
};
// the conversion should be what the bidder would need to return in order to match the actual floor
// rubicon
expect(bidRequest.getFloor()).to.deep.equal({
currency: 'USD',
floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust
});
// appnexus
expect(appnexusBid.getFloor()).to.deep.equal({
currency: 'USD',
floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points)
});
});
it('should use standard cpmAdjustment if no bidder cpmAdjustment', function () {
getGlobal().bidderSettings = {
rubicon: {
bidCpmAdjustment: function (bidCpm, bidResponse) {
return bidResponse.cpm * 0.5;
},
},
standard: {
bidCpmAdjustment: function (bidCpm, bidResponse) {
return bidResponse.cpm * 0.75;
},
}
};
_floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig);
_floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 };
let appnexusBid = {
...bidRequest,
bidder: 'appnexus'
};
// the conversion should be what the bidder would need to return in order to match the actual floor
// rubicon
expect(bidRequest.getFloor()).to.deep.equal({
currency: 'USD',
floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust
});
// appnexus
expect(appnexusBid.getFloor()).to.deep.equal({
currency: 'USD',
floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points)
});
});
it('should work when cpmAdjust function uses bid object', function () {
getGlobal().bidderSettings = {
rubicon: {
bidCpmAdjustment: function (bidCpm, bidResponse) {
return bidResponse.cpm * 0.5;
},
},
appnexus: {
bidCpmAdjustment: function (bidCpm, bidResponse) {
return bidResponse.cpm * 0.75;
},
}
};
_floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig);
_floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 };
let appnexusBid = {
...bidRequest,
bidder: 'appnexus'
};
// the conversion should be what the bidder would need to return in order to match the actual floor
// rubicon
expect(bidRequest.getFloor()).to.deep.equal({
currency: 'USD',
floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust
});
// appnexus
expect(appnexusBid.getFloor()).to.deep.equal({
currency: 'USD',
floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points)
});
});
it('should correctly pick the right attributes if * is passed in and context can be assumed', function () {
let inputBidReq = {
bidder: 'rubicon',
adUnitCode: 'test_div_2',
auctionId: '987654321',
mediaTypes: {
video: {}
},
getFloor
};
_floorDataForAuction[inputBidReq.auctionId] = utils.deepClone(basicFloorConfig);
_floorDataForAuction[inputBidReq.auctionId].data.values = {
'*': 1.0,
'banner': 3.0,
'video': 5.0
};
// because bid req only has video, if a bidder asks for a floor for * we can actually give them the right mediaType
expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({
currency: 'USD',
floor: 5.0 // 'video': 5.0
});
delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs;
// Same for if only banner is in the input bid
inputBidReq.mediaTypes = {banner: {}};
expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({
currency: 'USD',
floor: 3.0 // 'banner': 3.0,
});
delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs;
// if both are present then it will really use the *
inputBidReq.mediaTypes = {banner: {}, video: {}};
expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({
currency: 'USD',
floor: 1.0 // '*': 1.0,
});
delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs;
// now if size can be inferred (meaning only one size is in the specified mediaType, it will use it)
_floorDataForAuction[inputBidReq.auctionId].data.schema.fields = ['mediaType', 'size'];
_floorDataForAuction[inputBidReq.auctionId].data.values = {
'*|*': 1.0,
'banner|300x250': 2.0,
'banner|728x90': 3.0,
'banner|*': 4.0,
'video|300x250': 5.0,
'video|728x90': 6.0,
'video|*': 7.0
};
// mediaType is banner and only one size, so if someone asks for banner * we should give them banner 300x250
// instead of banner|*
inputBidReq.mediaTypes = {banner: {sizes: [[300, 250]]}};
expect(inputBidReq.getFloor({mediaType: 'banner', size: '*'})).to.deep.equal({
currency: 'USD',
floor: 2.0 // 'banner|300x250': 2.0,
});
delete _floorDataForAuction[inputBidReq.auctionId].data.match