mk9-prebid
Version:
Header Bidding Management Library
751 lines (640 loc) • 24.7 kB
JavaScript
import { expect } from 'chai';
import find from 'core-js-pure/features/array/find.js';
import { config } from 'src/config.js';
import * as utils from 'src/utils.js';
import { newStorageManager } from 'src/storageManager.js';
import { getRefererInfo } from 'src/refererDetection.js';
import { uspDataHandler } from 'src/adapterManager.js';
import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js';
import { parrableIdSubmodule } from 'modules/parrableIdSystem.js';
import { server } from 'test/mocks/xhr.js';
const storage = newStorageManager();
const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT';
const EXPIRE_COOKIE_TIME = 864000000;
const P_COOKIE_NAME = '_parrable_id';
const P_COOKIE_EID = '01.1563917337.test-eid';
const P_XHR_EID = '01.1588030911.test-new-eid'
const P_CONFIG_MOCK = {
name: 'parrableId',
params: {
partners: 'parrable_test_partner_123,parrable_test_partner_456'
}
};
const RESPONSE_HEADERS = { 'Content-Type': 'application/json' };
function getConfigMock() {
return {
userSync: {
syncDelay: 0,
userIds: [P_CONFIG_MOCK]
}
}
}
function getAdUnitMock(code = 'adUnit-code') {
return {
code,
mediaTypes: {banner: {}, native: {}},
sizes: [
[300, 200],
[300, 600]
],
bids: [{
bidder: 'sampleBidder',
params: { placementId: 'banner-only-bidder' }
}]
};
}
function serializeParrableId(parrableId) {
let str = '';
if (parrableId.eid) {
str += 'eid:' + parrableId.eid;
}
if (parrableId.ibaOptout) {
str += ',ibaOptout:1';
}
if (parrableId.ccpaOptout) {
str += ',ccpaOptout:1';
}
if (parrableId.tpc !== undefined) {
const tpcSupportComponent = parrableId.tpc === true ? 'tpc:1' : 'tpc:0';
str += `,${tpcSupportComponent}`;
str += `,tpcUntil:${parrableId.tpcUntil}`;
}
if (parrableId.filteredUntil) {
str += `,filteredUntil:${parrableId.filteredUntil}`;
str += `,filterHits:${parrableId.filterHits}`;
}
return str;
}
function writeParrableCookie(parrableId) {
let cookieValue = encodeURIComponent(serializeParrableId(parrableId));
storage.setCookie(
P_COOKIE_NAME,
cookieValue,
(new Date(Date.now() + EXPIRE_COOKIE_TIME).toUTCString()),
'lax'
);
}
function removeParrableCookie() {
storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE);
}
function decodeBase64UrlSafe(encBase64) {
const DEC = {
'-': '+',
'_': '/',
'.': '='
};
return encBase64.replace(/[-_.]/g, (m) => DEC[m]);
}
describe('Parrable ID System', function() {
describe('parrableIdSystem.getId()', function() {
describe('response callback function', function() {
let logErrorStub;
let callbackSpy = sinon.spy();
beforeEach(function() {
logErrorStub = sinon.stub(utils, 'logError');
callbackSpy.resetHistory();
writeParrableCookie({ eid: P_COOKIE_EID });
});
afterEach(function() {
removeParrableCookie();
logErrorStub.restore();
})
it('creates xhr to Parrable that synchronizes the ID', function() {
let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK);
getIdResult.callback(callbackSpy);
let request = server.requests[0];
let queryParams = utils.parseQS(request.url.split('?')[1]);
let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data)));
expect(getIdResult.callback).to.be.a('function');
expect(request.url).to.contain('h.parrable.com');
expect(queryParams).to.not.have.property('us_privacy');
expect(data).to.deep.equal({
eid: P_COOKIE_EID,
trackers: P_CONFIG_MOCK.params.partners.split(','),
url: getRefererInfo().referer,
prebidVersion: '$prebid.version$',
isIframe: true
});
server.requests[0].respond(200,
{ 'Content-Type': 'text/plain' },
JSON.stringify({ eid: P_XHR_EID })
);
expect(callbackSpy.lastCall.lastArg).to.deep.equal({
eid: P_XHR_EID
});
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID)
);
});
it('xhr passes the uspString to Parrable', function() {
let uspString = '1YNN';
uspDataHandler.setConsentData(uspString);
parrableIdSubmodule.getId(
P_CONFIG_MOCK,
null,
null
).callback(callbackSpy);
uspDataHandler.setConsentData(null);
expect(server.requests[0].url).to.contain('us_privacy=' + uspString);
});
it('xhr base64 safely encodes url data object', function() {
const urlSafeBase64EncodedData = '-_.';
const btoaStub = sinon.stub(window, 'btoa').returns('+/=');
let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK);
getIdResult.callback(callbackSpy);
let request = server.requests[0];
let queryParams = utils.parseQS(request.url.split('?')[1]);
expect(queryParams.data).to.equal(urlSafeBase64EncodedData);
btoaStub.restore();
});
it('should log an error and continue to callback if ajax request errors', function () {
let callBackSpy = sinon.spy();
let submoduleCallback = parrableIdSubmodule.getId({ params: {partners: 'prebid'} }).callback;
submoduleCallback(callBackSpy);
let request = server.requests[0];
expect(request.url).to.contain('h.parrable.com');
request.respond(
503,
null,
'Unavailable'
);
expect(logErrorStub.calledOnce).to.be.true;
expect(callBackSpy.calledOnce).to.be.true;
});
});
describe('response id', function() {
it('provides the stored Parrable values if a cookie exists', function() {
writeParrableCookie({ eid: P_COOKIE_EID });
let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK);
removeParrableCookie();
expect(getIdResult.id).to.deep.equal({
eid: P_COOKIE_EID
});
});
it('provides the stored legacy Parrable ID values if cookies exist', function() {
let oldEid = '01.111.old-eid';
let oldEidCookieName = '_parrable_eid';
let oldOptoutCookieName = '_parrable_optout';
storage.setCookie(oldEidCookieName, oldEid);
storage.setCookie(oldOptoutCookieName, 'true');
let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK);
expect(getIdResult.id).to.deep.equal({
eid: oldEid,
ibaOptout: true
});
// The ID system is expected to migrate old cookies to the new format
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + oldEid + ',ibaOptout:1')
);
expect(storage.getCookie(oldEidCookieName)).to.equal(null);
expect(storage.getCookie(oldOptoutCookieName)).to.equal(null);
removeParrableCookie();
});
});
describe('GDPR consent', () => {
let callbackSpy = sinon.spy();
const config = {
params: {
partner: 'partner'
}
};
const gdprConsentTestCases = [
{ consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: { gdpr: 1, gdpr_consent: 'expectedConsentString' } },
{ consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: { gdpr: 0 } },
{ consentData: { gdprApplies: true, consentString: undefined }, expected: { gdpr: 1, gdpr_consent: '' } },
{ consentData: { gdprApplies: 'yes', consentString: 'expectedConsentString' }, expected: { gdpr: 0 } },
{ consentData: undefined, expected: { gdpr: 0 } }
];
gdprConsentTestCases.forEach((testCase, index) => {
it(`should call user sync url with the gdprConsent - case ${index}`, () => {
parrableIdSubmodule.getId(config, testCase.consentData).callback(callbackSpy);
if (testCase.expected.gdpr === 1) {
expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr);
expect(server.requests[0].url).to.contain('gdpr_consent=' + testCase.expected.gdpr_consent);
} else {
expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr);
expect(server.requests[0].url).to.not.contain('gdpr_consent');
}
})
});
});
describe('third party cookie support', function () {
let logErrorStub;
let callbackSpy = sinon.spy();
beforeEach(function() {
logErrorStub = sinon.stub(utils, 'logError');
});
afterEach(function () {
callbackSpy.resetHistory();
removeParrableCookie();
});
afterEach(function() {
logErrorStub.restore();
});
describe('when getting tpcSupport from XHR response', function () {
let request;
let dateNowStub;
const dateNowMock = Date.now();
const tpcSupportTtl = 1;
before(() => {
dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock);
});
after(() => {
dateNowStub.restore();
});
it('should set tpcSupport: true and tpcUntil in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, tpcSupport: true, tpcSupportTtl })
);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID + ',tpc:1,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl))
);
});
it('should set tpcSupport: false and tpcUntil in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, tpcSupport: false, tpcSupportTtl })
);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID + ',tpc:0,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl))
);
});
it('should not set tpcSupport in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID })
);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID)
);
});
});
});
describe('request-filter status', function () {
let logErrorStub;
let callbackSpy = sinon.spy();
beforeEach(function() {
logErrorStub = sinon.stub(utils, 'logError');
});
afterEach(function () {
callbackSpy.resetHistory();
removeParrableCookie();
});
afterEach(function() {
logErrorStub.restore();
});
describe('when getting filterTtl from XHR response', function () {
let request;
let dateNowStub;
const dateNowMock = Date.now();
const filterTtl = 1000;
before(() => {
dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock);
});
after(() => {
dateNowStub.restore();
});
it('should set filteredUntil in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, filterTtl })
);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent(
'eid:' + P_XHR_EID +
',filteredUntil:' + Math.floor((dateNowMock / 1000) + filterTtl) +
',filterHits:0')
);
});
it('should increment filterHits in the cookie', function () {
writeParrableCookie({
eid: P_XHR_EID,
filteredUntil: Math.floor((dateNowMock / 1000) + filterTtl),
filterHits: 0
});
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent(
'eid:' + P_XHR_EID +
',filteredUntil:' + Math.floor((dateNowMock / 1000) + filterTtl) +
',filterHits:1')
);
});
it('should send filterHits in the XHR', function () {
const filterHits = 1;
writeParrableCookie({
eid: P_XHR_EID,
filteredUntil: Math.floor(dateNowMock / 1000),
filterHits
});
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
let queryParams = utils.parseQS(request.url.split('?')[1]);
let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data)));
expect(data.filterHits).to.equal(filterHits);
});
});
});
});
describe('parrableIdSystem.decode()', function() {
it('provides the Parrable ID (EID) from a stored object', function() {
let eid = '01.123.4567890';
let parrableId = {
eid,
ibaOptout: true
};
expect(parrableIdSubmodule.decode(parrableId)).to.deep.equal({
parrableId
});
});
});
describe('timezone filtering', function() {
before(function() {
sinon.stub(Intl, 'DateTimeFormat');
});
after(function() {
Intl.DateTimeFormat.restore();
});
it('permits an impression when no timezoneFilter is configured', function() {
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
} })).to.have.property('callback');
});
it('permits an impression from a blocked timezone when a cookie exists', function() {
const blockedZone = 'Antarctica/South_Pole';
const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone });
Intl.DateTimeFormat.returns({ resolvedOptions });
writeParrableCookie({ eid: P_COOKIE_EID });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedZones: [ blockedZone ]
}
} })).to.have.property('callback');
expect(resolvedOptions.called).to.equal(false);
removeParrableCookie();
})
it('permits an impression from an allowed timezone', function() {
const allowedZone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
allowedZones: [ allowedZone ]
}
} })).to.have.property('callback');
expect(resolvedOptions.called).to.equal(true);
});
it('permits an impression from a lower cased allowed timezone', function() {
const allowedZone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partner: 'prebid-test',
timezoneFilter: {
allowedZones: [ allowedZone.toLowerCase() ]
}
} })).to.have.property('callback');
expect(resolvedOptions.called).to.equal(true);
});
it('permits an impression from a timezone that is not blocked', function() {
const blockedZone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: 'Iceland' });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedZones: [ blockedZone ]
}
} })).to.have.property('callback');
expect(resolvedOptions.called).to.equal(true);
});
it('does not permit an impression from a blocked timezone', function() {
const blockedZone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedZones: [ blockedZone ]
}
} })).to.equal(null);
expect(resolvedOptions.called).to.equal(true);
});
it('does not permit an impression from a lower cased blocked timezone', function() {
const blockedZone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partner: 'prebid-test',
timezoneFilter: {
blockedZones: [ blockedZone.toLowerCase() ]
}
} })).to.equal(null);
expect(resolvedOptions.called).to.equal(true);
});
it('does not permit an impression from a blocked timezone even when also allowed', function() {
const timezone = 'America/New_York';
const resolvedOptions = sinon.stub().returns({ timeZone: timezone });
Intl.DateTimeFormat.returns({ resolvedOptions });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
allowedZones: [ timezone ],
blockedZones: [ timezone ]
}
} })).to.equal(null);
expect(resolvedOptions.called).to.equal(true);
});
});
describe('timezone offset filtering', function() {
before(function() {
sinon.stub(Date.prototype, 'getTimezoneOffset');
});
afterEach(function() {
Date.prototype.getTimezoneOffset.reset();
})
after(function() {
Date.prototype.getTimezoneOffset.restore();
});
it('permits an impression from a blocked offset when a cookie exists', function() {
const blockedOffset = -4;
Date.prototype.getTimezoneOffset.returns(blockedOffset * 60);
writeParrableCookie({ eid: P_COOKIE_EID });
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedOffsets: [ blockedOffset ]
}
} })).to.have.property('callback');
removeParrableCookie();
});
it('permits an impression from an allowed offset', function() {
const allowedOffset = -5;
Date.prototype.getTimezoneOffset.returns(allowedOffset * 60);
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
allowedOffsets: [ allowedOffset ]
}
} })).to.have.property('callback');
expect(Date.prototype.getTimezoneOffset.called).to.equal(true);
});
it('permits an impression from an offset that is not blocked', function() {
const allowedOffset = -5;
const blockedOffset = 5;
Date.prototype.getTimezoneOffset.returns(allowedOffset * 60);
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedOffsets: [ blockedOffset ]
}
}})).to.have.property('callback');
expect(Date.prototype.getTimezoneOffset.called).to.equal(true);
});
it('does not permit an impression from a blocked offset', function() {
const blockedOffset = -5;
Date.prototype.getTimezoneOffset.returns(blockedOffset * 60);
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
blockedOffsets: [ blockedOffset ]
}
} })).to.equal(null);
expect(Date.prototype.getTimezoneOffset.called).to.equal(true);
});
it('does not permit an impression from a blocked offset even when also allowed', function() {
const offset = -5;
Date.prototype.getTimezoneOffset.returns(offset * 60);
expect(parrableIdSubmodule.getId({ params: {
partners: 'prebid-test',
timezoneFilter: {
allowedOffset: [ offset ],
blockedOffsets: [ offset ]
}
} })).to.equal(null);
expect(Date.prototype.getTimezoneOffset.called).to.equal(true);
});
});
describe('userId requestBids hook', function() {
let adUnits;
beforeEach(function() {
adUnits = [getAdUnitMock()];
writeParrableCookie({ eid: P_COOKIE_EID, ibaOptout: true });
setSubmoduleRegistry([parrableIdSubmodule]);
init(config);
config.setConfig(getConfigMock());
});
afterEach(function() {
removeParrableCookie();
storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE);
});
it('when a stored Parrable ID exists it is added to bids', function(done) {
requestBidsHook(function() {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property('userId.parrableId');
expect(bid.userId.parrableId.eid).to.equal(P_COOKIE_EID);
expect(bid.userId.parrableId.ibaOptout).to.equal(true);
const parrableIdAsEid = find(bid.userIdAsEids, e => e.source == 'parrable.com');
expect(parrableIdAsEid).to.deep.equal({
source: 'parrable.com',
uids: [{
id: P_COOKIE_EID,
atype: 1,
ext: {
ibaOptout: true
}
}]
});
});
});
done();
}, { adUnits });
});
it('supplies an optout reason when the EID is missing due to CCPA non-consent', function(done) {
// the ID system itself will not write a cookie with an EID when CCPA=true
writeParrableCookie({ ccpaOptout: true });
requestBidsHook(function() {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property('userId.parrableId');
expect(bid.userId.parrableId).to.not.have.property('eid');
expect(bid.userId.parrableId.ccpaOptout).to.equal(true);
const parrableIdAsEid = find(bid.userIdAsEids, e => e.source == 'parrable.com');
expect(parrableIdAsEid).to.deep.equal({
source: 'parrable.com',
uids: [{
id: '',
atype: 1,
ext: {
ccpaOptout: true
}
}]
});
});
});
done();
}, { adUnits });
});
});
describe('partners parsing', function () {
let callbackSpy = sinon.spy();
const partnersTestCase = [
{
name: '"partners" as an array',
config: { params: { partners: ['parrable_test_partner_123', 'parrable_test_partner_456'] } },
expected: ['parrable_test_partner_123', 'parrable_test_partner_456']
},
{
name: '"partners" as a string list',
config: { params: { partners: 'parrable_test_partner_123,parrable_test_partner_456' } },
expected: ['parrable_test_partner_123', 'parrable_test_partner_456']
},
{
name: '"partners" as a string',
config: { params: { partners: 'parrable_test_partner_123' } },
expected: ['parrable_test_partner_123']
},
{
name: '"partner" as a string list',
config: { params: { partner: 'parrable_test_partner_123,parrable_test_partner_456' } },
expected: ['parrable_test_partner_123', 'parrable_test_partner_456']
},
{
name: '"partner" as string',
config: { params: { partner: 'parrable_test_partner_123' } },
expected: ['parrable_test_partner_123']
},
];
partnersTestCase.forEach(testCase => {
it(`accepts config property ${testCase.name}`, () => {
parrableIdSubmodule.getId(testCase.config).callback(callbackSpy);
let request = server.requests[0];
let queryParams = utils.parseQS(request.url.split('?')[1]);
let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data)));
expect(data.trackers).to.deep.equal(testCase.expected);
});
});
});
});