mk9-prebid
Version:
Header Bidding Management Library
509 lines (414 loc) • 19.5 kB
JavaScript
import {
id5IdSubmodule,
ID5_STORAGE_NAME,
ID5_PRIVACY_STORAGE_NAME,
getFromLocalStorage,
storeInLocalStorage,
expDaysStr,
nbCacheName,
getNbFromCache,
storeNbInCache,
isInControlGroup
} from 'modules/id5IdSystem.js';
import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js';
import { config } from 'src/config.js';
import { server } from 'test/mocks/xhr.js';
import events from 'src/events.js';
import CONSTANTS from 'src/constants.json';
import * as utils from 'src/utils.js';
let expect = require('chai').expect;
describe('ID5 ID System', function() {
const ID5_MODULE_NAME = 'id5Id';
const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase();
const ID5_SOURCE = 'id5-sync.com';
const ID5_TEST_PARTNER_ID = 173;
const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`;
const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID);
const ID5_STORED_ID = 'storedid5id';
const ID5_STORED_SIGNATURE = '123456';
const ID5_STORED_LINK_TYPE = 1;
const ID5_STORED_OBJ = {
'universal_uid': ID5_STORED_ID,
'signature': ID5_STORED_SIGNATURE,
'link_type': ID5_STORED_LINK_TYPE
};
const ID5_RESPONSE_ID = 'newid5id';
const ID5_RESPONSE_SIGNATURE = 'abcdef';
const ID5_RESPONSE_LINK_TYPE = 2;
const ID5_JSON_RESPONSE = {
'universal_uid': ID5_RESPONSE_ID,
'signature': ID5_RESPONSE_SIGNATURE,
'link_type': ID5_RESPONSE_LINK_TYPE
};
function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') {
return {
name: ID5_MODULE_NAME,
params: {
partner: ID5_TEST_PARTNER_ID
},
storage: {
name: storageName,
type: storageType,
expires: 90
}
}
}
function getId5ValueConfig(value) {
return {
name: ID5_MODULE_NAME,
value: {
id5id: {
uid: value
}
}
}
}
function getUserSyncConfig(userIds) {
return {
userSync: {
userIds: userIds,
syncDelay: 0
}
}
}
function getFetchLocalStorageConfig() {
return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]);
}
function getValueConfig(value) {
return getUserSyncConfig([getId5ValueConfig(value)]);
}
function getAdUnitMock(code = 'adUnit-code') {
return {
code,
mediaTypes: {banner: {}, native: {}},
sizes: [[300, 200], [300, 600]],
bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}]
};
}
describe('Check for valid publisher config', function() {
it('should fail with invalid config', function() {
// no config
expect(id5IdSubmodule.getId()).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ })).to.be.eq(undefined);
// valid params, invalid storage
expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: {} })).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { name: '' } })).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { type: '' } })).to.be.eq(undefined);
// valid storage, invalid params
expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, })).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { } })).to.be.eq(undefined);
expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined);
});
it('should warn with non-recommended storage params', function() {
let logWarnStub = sinon.stub(utils, 'logWarn');
id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } });
expect(logWarnStub.calledOnce).to.be.true;
logWarnStub.restore();
id5IdSubmodule.getId({ storage: { name: ID5_STORAGE_NAME, type: 'cookie', }, params: { partner: 123 } });
expect(logWarnStub.calledOnce).to.be.true;
logWarnStub.restore();
});
});
describe('Xhr Requests from getId()', function() {
const responseHeader = { 'Content-Type': 'application/json' };
let callbackSpy = sinon.spy();
beforeEach(function() {
callbackSpy.resetHistory();
});
afterEach(function () {
});
it('should call the ID5 server and handle a valid response', function () {
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(request.url).to.contain(ID5_ENDPOINT);
expect(request.withCredentials).to.be.true;
expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID);
expect(requestBody.o).to.eq('pbjs');
expect(requestBody.pd).to.be.undefined;
expect(requestBody.s).to.be.undefined;
expect(requestBody.provider).to.be.undefined
expect(requestBody.v).to.eq('$prebid.version$');
expect(requestBody.gdpr).to.exist;
expect(requestBody.gdpr_consent).to.be.undefined;
expect(requestBody.us_privacy).to.be.undefined;
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
expect(callbackSpy.calledOnce).to.be.true;
expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE);
});
it('should call the ID5 server with no signature field when no stored object', function () {
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.s).to.be.undefined;
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server with signature field from stored object', function () {
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE);
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server with pd field when pd config is set', function () {
const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3';
let id5Config = getId5FetchConfig();
id5Config.params.pd = pubData;
let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.pd).to.eq(pubData);
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server with no pd field when pd config is not set', function () {
let id5Config = getId5FetchConfig();
id5Config.params.pd = undefined;
let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.pd).to.be.undefined;
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () {
coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME);
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.nbPage).to.eq(1);
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0);
});
it('should call the ID5 server with incremented nb when stored value exists and reset after', function () {
storeNbInCache(ID5_TEST_PARTNER_ID, 1);
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.nbPage).to.eq(2);
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0);
});
it('should call the ID5 server with ab_testing object when abTesting is turned on', function () {
let id5Config = getId5FetchConfig();
id5Config.params.abTesting = { enabled: true, controlGroupPct: 0.234 }
let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.ab_testing.enabled).to.eq(true);
expect(requestBody.ab_testing.control_group_pct).to.eq(0.234);
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server without ab_testing object when abTesting is turned off', function () {
let id5Config = getId5FetchConfig();
id5Config.params.abTesting = { enabled: false, controlGroupPct: 0.55 }
let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.ab_testing).to.be.undefined;
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should call the ID5 server without ab_testing when when abTesting is not set', function () {
let id5Config = getId5FetchConfig();
let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(requestBody.ab_testing).to.be.undefined;
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
});
it('should store the privacy object from the ID5 server response', function () {
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
let responseObject = utils.deepClone(ID5_JSON_RESPONSE);
responseObject.privacy = {
jurisdiction: 'gdpr',
id5_consent: true
};
request.respond(200, responseHeader, JSON.stringify(responseObject));
expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.eq(JSON.stringify(responseObject.privacy));
coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME);
});
it('should not store a privacy object if not part of ID5 server response', function () {
coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME);
let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback;
submoduleCallback(callbackSpy);
let request = server.requests[0];
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.null;
});
});
describe('Request Bids Hook', function() {
let adUnits;
beforeEach(function() {
sinon.stub(events, 'getEvents').returns([]);
coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME);
coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`);
coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME);
adUnits = [getAdUnitMock()];
});
afterEach(function() {
events.getEvents.restore();
coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME);
coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`);
coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME);
});
it('should add stored ID from cache to bids', function (done) {
storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1);
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getFetchLocalStorageConfig());
requestBidsHook(function () {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`);
expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID);
expect(bid.userIdAsEids[0]).to.deep.equal({
source: ID5_SOURCE,
uids: [{
id: ID5_STORED_ID,
atype: 1,
ext: {
linkType: ID5_STORED_LINK_TYPE
}
}]
});
});
});
done();
}, { adUnits });
});
it('should add config value ID to bids', function (done) {
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getValueConfig(ID5_STORED_ID));
requestBidsHook(function () {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`);
expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID);
expect(bid.userIdAsEids[0]).to.deep.equal({
source: ID5_SOURCE,
uids: [{ id: ID5_STORED_ID, atype: 1 }]
});
});
});
done();
}, { adUnits });
});
it('should set nb=1 in cache when no stored nb value exists and cached ID', function () {
storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1);
coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME);
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getFetchLocalStorageConfig());
let innerAdUnits;
requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits});
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1);
});
it('should increment nb in cache when stored nb value exists and cached ID', function () {
storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1);
storeNbInCache(ID5_TEST_PARTNER_ID, 1);
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getFetchLocalStorageConfig());
let innerAdUnits;
requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits});
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2);
});
it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () {
storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1);
storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1);
storeNbInCache(ID5_TEST_PARTNER_ID, 1);
let id5Config = getFetchLocalStorageConfig();
id5Config.userSync.userIds[0].storage.refreshInSeconds = 2;
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(id5Config);
let innerAdUnits;
requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits});
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2);
expect(server.requests).to.be.empty;
events.emit(CONSTANTS.EVENTS.AUCTION_END, {});
let request = server.requests[0];
let requestBody = JSON.parse(request.requestBody);
expect(request.url).to.contain(ID5_ENDPOINT);
expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE);
expect(requestBody.nbPage).to.eq(2);
const responseHeader = { 'Content-Type': 'application/json' };
request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE));
expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0);
});
});
describe('Decode stored object', function() {
const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } };
it('should properly decode from a stored object', function() {
expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject);
});
it('should return undefined if passed a string', function() {
expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined);
});
});
describe('A/B Testing', function() {
const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } };
const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } };
const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: '', ext: { linkType: 0, abTestingControlGroup: true } } };
let testConfig, storedObject;
beforeEach(function() {
testConfig = getId5FetchConfig();
storedObject = utils.deepClone(ID5_STORED_OBJ);
});
describe('A/B Testing Config is Set', function() {
let randStub;
beforeEach(function() {
randStub = sinon.stub(Math, 'random').callsFake(function() {
return 0.25;
});
});
afterEach(function () {
randStub.restore();
});
describe('Decode', function() {
let logErrorSpy;
beforeEach(function() {
logErrorSpy = sinon.spy(utils, 'logError');
});
afterEach(function() {
logErrorSpy.restore();
});
it('should not set abTestingControlGroup extension when A/B testing is off', function () {
let decoded = id5IdSubmodule.decode(storedObject, testConfig);
expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff);
});
it('should set abTestingControlGroup to false when A/B testing is on but in normal group', function () {
storedObject.ab_testing = { result: 'normal' };
let decoded = id5IdSubmodule.decode(storedObject, testConfig);
expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn);
});
it('should not expose ID when everyone is in control group', function () {
storedObject.ab_testing = { result: 'control' };
storedObject.universal_uid = '';
storedObject.link_type = 0;
let decoded = id5IdSubmodule.decode(storedObject, testConfig);
expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn);
});
it('should log A/B testing errors', function () {
storedObject.ab_testing = { result: 'error' };
let decoded = id5IdSubmodule.decode(storedObject, testConfig);
expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff);
sinon.assert.calledOnce(logErrorSpy);
});
});
});
});
});