mk9-prebid
Version:
Header Bidding Management Library
543 lines (446 loc) • 18.7 kB
JavaScript
import {assert} from 'chai';
import {spec} from 'modules/stroeerCoreBidAdapter.js';
import * as utils from 'src/utils.js';
import {BANNER, VIDEO} from '../../../src/mediaTypes.js';
import find from 'core-js-pure/features/array/find.js';
describe('stroeerCore bid adapter', function () {
let sandbox;
let fakeServer;
let bidderRequest;
let clock;
beforeEach(() => {
bidderRequest = buildBidderRequest();
sandbox = sinon.sandbox.create();
fakeServer = sandbox.useFakeServer();
clock = sandbox.useFakeTimers();
});
afterEach(() => {
sandbox.restore();
});
function assertStandardFieldsOnBid(bidObject, bidId, ad, width, height, cpm) {
assert.propertyVal(bidObject, 'requestId', bidId);
assert.propertyVal(bidObject, 'ad', ad);
assert.propertyVal(bidObject, 'width', width);
assert.propertyVal(bidObject, 'height', height);
assert.propertyVal(bidObject, 'cpm', cpm);
assert.propertyVal(bidObject, 'currency', 'EUR');
assert.propertyVal(bidObject, 'netRevenue', true);
assert.propertyVal(bidObject, 'creativeId', '');
}
const AUCTION_ID = utils.getUniqueIdentifierStr();
// Vendor user ids and associated data
const userIds = Object.freeze({
criteoId: 'criteo-user-id',
digitrustid: {
data: {
id: 'encrypted-user-id==',
keyv: 4,
privacy: {optout: false},
producer: 'ABC',
version: 2
}
},
lipb: {
lipbid: 'T7JiRRvsRAmh88',
segments: ['999']
}
});
const buildBidderRequest = () => ({
auctionId: AUCTION_ID,
bidderRequestId: 'bidder-request-id-123',
bidderCode: 'stroeerCore',
timeout: 5000,
auctionStart: 10000,
bids: [{
bidId: 'bid1',
bidder: 'stroeerCore',
adUnitCode: 'div-1',
mediaTypes: {
banner: {
sizes: [[300, 600], [160, 60]]
}
},
params: {
sid: 'NDA='
},
userId: userIds
}, {
bidId: 'bid2',
bidder: 'stroeerCore',
adUnitCode: 'div-2',
mediaTypes: {
banner: {
sizes: [[728, 90]],
}
},
params: {
sid: 'ODA='
},
userId: userIds
}],
});
const buildBidderRequestPreVersion3 = () => {
const request = buildBidderRequest();
request.bids.forEach((bid) => {
bid.sizes = bid.mediaTypes.banner.sizes;
delete bid.mediaTypes;
bid.mediaType = 'banner';
});
return request;
};
const buildBidderResponse = () => ({
'bids': [{
'bidId': 'bid1', 'cpm': 4.0, 'width': 300, 'height': 600, 'ad': '<div>tag1</div>', 'tracking': {'brandId': 123}
}, {
'bidId': 'bid2', 'cpm': 7.3, 'width': 728, 'height': 90, 'ad': '<div>tag2</div>'
}]
});
const createWindow = (href, params = {}) => {
let {parent, referrer, top, frameElement, placementElements = []} = params;
const protocol = (href.indexOf('https') === 0) ? 'https:' : 'http:';
const win = {
frameElement,
parent,
top,
location: {
protocol, href
},
document: {
createElement: function () {
return {
setAttribute: function () {
}
}
},
referrer,
getElementById: id => find(placementElements, el => el.id === id)
}
};
win.self = win;
if (!parent) {
win.parent = win;
}
if (!top) {
win.top = win;
}
return win;
};
function createElement(id, offsetTop = 0) {
return {
id,
getBoundingClientRect: function () {
return {
top: offsetTop, height: 1
}
}
}
}
function setupSingleWindow(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) {
const win = createWindow('http://www.xyz.com/', {
parent: win, top: win, frameElement: createElement(undefined, 304), placementElements: placementElements
});
win.innerHeight = 200;
sandBox.stub(utils, 'getWindowSelf').returns(win);
sandBox.stub(utils, 'getWindowTop').returns(win);
return win;
}
function setupNestedWindows(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) {
const topWin = createWindow('http://www.abc.org/', {referrer: 'http://www.google.com/?query=monkey'});
topWin.innerHeight = 800;
const midWin = createWindow('http://www.abc.org/', {parent: topWin, top: topWin, frameElement: createElement()});
midWin.innerHeight = 400;
const win = createWindow('http://www.xyz.com/', {
parent: midWin, top: topWin, frameElement: createElement(undefined, 304), placementElements
});
win.innerHeight = 200;
sandBox.stub(utils, 'getWindowSelf').returns(win);
sandBox.stub(utils, 'getWindowTop').returns(topWin);
return {topWin, midWin, win};
}
it('should only support BANNER mediaType', function () {
assert.deepEqual(spec.supportedMediaTypes, [BANNER]);
});
describe('bid validation entry point', () => {
let bidRequest;
beforeEach(() => {
bidRequest = buildBidderRequest().bids[0];
});
it('should have \"isBidRequestValid\" function', () => {
assert.isFunction(spec.isBidRequestValid);
});
it('should pass a valid bid', () => {
assert.isTrue(spec.isBidRequestValid(bidRequest));
});
it('should exclude bids without slot id param', () => {
bidRequest.params.sid = undefined;
assert.isFalse(spec.isBidRequestValid(bidRequest));
});
it('should exclude non-banner bids', () => {
delete bidRequest.mediaTypes.banner;
bidRequest.mediaTypes.video = {
playerSize: [640, 480]
};
assert.isFalse(spec.isBidRequestValid(bidRequest));
});
it('should exclude non-banner, pre-version 3 bids', () => {
delete bidRequest.mediaTypes;
bidRequest.mediaType = VIDEO;
assert.isFalse(spec.isBidRequestValid(bidRequest));
});
});
describe('build request entry point', () => {
it('should have \"buildRequests\" function', () => {
assert.isFunction(spec.buildRequests);
});
describe('url on server request info object', () => {
let win;
beforeEach(() => {
win = setupSingleWindow(sandbox);
});
afterEach(() => {
sandbox.restore();
});
it('should use hardcoded url as default endpoint', () => {
const bidReq = buildBidderRequest();
let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
assert.equal(serverRequestInfo.method, 'POST');
assert.isObject(serverRequestInfo.data);
assert.equal(serverRequestInfo.url, 'https://hb.adscale.de/dsh');
});
describe('should use custom url if provided', () => {
const samples = [{
protocol: 'http:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz'
}, {
protocol: 'https:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz'
}, {
protocol: 'https:',
params: {sid: 'ODA=', host: 'other.com', port: '234', securePort: '871', path: '/xyz'},
expected: 'https://other.com:871/xyz'
}, {
protocol: 'http:', params: {sid: 'ODA=', port: '234', path: '/xyz'}, expected: 'https://hb.adscale.de:234/xyz'
}, ];
samples.forEach(sample => {
it(`should use ${sample.expected} as endpoint when given params ${JSON.stringify(sample.params)} and protocol ${sample.protocol}`,
function () {
win.location.protocol = sample.protocol;
const bidReq = buildBidderRequest();
bidReq.bids[0].params = sample.params;
bidReq.bids.length = 1;
let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
assert.equal(serverRequestInfo.method, 'POST');
assert.isObject(serverRequestInfo.data);
assert.equal(serverRequestInfo.url, sample.expected);
});
});
});
});
describe('payload on server request info object', () => {
let topWin;
let win;
let placementElements;
beforeEach(() => {
placementElements = [createElement('div-1', 17), createElement('div-2', 54)];
({ topWin, win } = setupNestedWindows(sandbox, placementElements));
});
afterEach(() => {
sandbox.restore();
});
it('should have expected JSON structure', () => {
clock.tick(13500);
const bidReq = buildBidderRequest();
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
const expectedTimeout = bidderRequest.timeout - (13500 - bidderRequest.auctionStart);
assert.equal(expectedTimeout, 1500);
const expectedJsonPayload = {
'id': AUCTION_ID,
'timeout': expectedTimeout,
'ref': topWin.document.referrer,
'mpa': true,
'ssl': false,
'bids': [{
'sid': 'NDA=', 'bid': 'bid1', 'siz': [[300, 600], [160, 60]], 'viz': true
}, {
'sid': 'ODA=', 'bid': 'bid2', 'siz': [[728, 90]], 'viz': true
}],
'user': {
'euids': userIds
}
};
// trim away fields with undefined
const actualJsonPayload = JSON.parse(JSON.stringify(serverRequestInfo.data));
assert.deepEqual(actualJsonPayload, expectedJsonPayload);
});
it('should handle banner sizes for pre version 3', () => {
// Version 3 changes the way how banner sizes are accessed.
// We can support backwards compatibility with version 2.x
const bidReq = buildBidderRequestPreVersion3();
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
assert.deepEqual(serverRequestInfo.data.bids[0].siz, [[300, 600], [160, 60]]);
assert.deepEqual(serverRequestInfo.data.bids[1].siz, [[728, 90]]);
});
describe('optional fields', () => {
it('should skip viz field when unable to determine visibility of placement', () => {
placementElements.length = 0;
const bidReq = buildBidderRequest();
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
assert.lengthOf(serverRequestInfo.data.bids, 2);
for (let bid of serverRequestInfo.data.bids) {
assert.isUndefined(bid.viz);
}
});
it('should skip ref field when unable to determine document referrer', () => {
// i.e., empty if user came from bookmark, or web page using 'rel="noreferrer" on link, etc
buildBidderRequest();
const serverRequestInfo = spec.buildRequests(bidderRequest.bids, bidderRequest);
assert.lengthOf(serverRequestInfo.data.bids, 2);
for (let bid of serverRequestInfo.data.bids) {
assert.isUndefined(bid.ref);
}
});
const gdprSamples = [{consentString: 'RG9ua2V5IEtvbmc=', gdprApplies: true}, {consentString: 'UGluZyBQb25n', gdprApplies: false}];
gdprSamples.forEach((sample) => {
it(`should add GDPR info ${JSON.stringify(sample)} when provided`, () => {
const bidReq = buildBidderRequest();
bidReq.gdprConsent = sample;
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
const actualGdpr = serverRequestInfo.data.gdpr;
assert.propertyVal(actualGdpr, 'applies', sample.gdprApplies);
assert.propertyVal(actualGdpr, 'consent', sample.consentString);
});
});
const skippableGdprSamples = [{consentString: null, gdprApplies: true}, //
{consentString: 'UGluZyBQb25n', gdprApplies: null}, //
{consentString: null, gdprApplies: null}, //
{consentString: undefined, gdprApplies: true}, //
{consentString: 'UGluZyBQb25n', gdprApplies: undefined}, //
{consentString: undefined, gdprApplies: undefined}];
skippableGdprSamples.forEach((sample) => {
it(`should not add GDPR info ${JSON.stringify(sample)} when one or more values are missing`, () => {
const bidReq = buildBidderRequest();
bidReq.gdprConsent = sample;
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
const actualGdpr = serverRequestInfo.data.gdpr;
assert.isUndefined(actualGdpr);
});
});
it('should be able to build without third party user id data', () => {
const bidReq = buildBidderRequest();
bidReq.bids.forEach(bid => delete bid.userId);
const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq);
assert.lengthOf(serverRequestInfo.data.bids, 2);
assert.notProperty(serverRequestInfo, 'uids');
});
});
});
});
describe('interpret response entry point', () => {
it('should have \"interpretResponse\" function', () => {
assert.isFunction(spec.interpretResponse);
});
const invalidResponses = ['', ' ', ' ', undefined, null];
invalidResponses.forEach(sample => {
it('should ignore invalid responses (\"' + sample + '\") response', () => {
const result = spec.interpretResponse({body: sample});
assert.isArray(result);
assert.lengthOf(result, 0);
});
});
it('should interpret a standard response', () => {
const bidderResponse = buildBidderResponse();
const result = spec.interpretResponse({body: bidderResponse});
assertStandardFieldsOnBid(result[0], 'bid1', '<div>tag1</div>', 300, 600, 4);
assertStandardFieldsOnBid(result[1], 'bid2', '<div>tag2</div>', 728, 90, 7.3);
});
it('should return empty array, when response contains no bids', () => {
const result = spec.interpretResponse({body: {bids: []}});
assert.deepStrictEqual(result, []);
});
it('should add data to meta object', () => {
const response = buildBidderResponse();
response.bids[0] = Object.assign(response.bids[0], {adomain: ['website.org', 'domain.com']});
const result = spec.interpretResponse({body: response});
assert.deepPropertyVal(result[0], 'meta', {advertiserDomains: ['website.org', 'domain.com']});
// nothing provided for the second bid
assert.deepPropertyVal(result[1], 'meta', {advertiserDomains: undefined});
});
});
describe('get user syncs entry point', () => {
let win;
beforeEach(() => {
win = setupSingleWindow(sandbox);
// fake
win.document.createElement = function () {
const attrs = {};
return {
setAttribute: (name, value) => {
attrs[name] = value
},
getAttribute: (name) => attrs[name],
hasAttribute: (name) => attrs[name] !== undefined,
tagName: 'SCRIPT',
}
}
});
it('should have \"getUserSyncs\" function', () => {
assert.isFunction(spec.getUserSyncs);
});
describe('when iframe option is enabled', () => {
it('should perform user connect when there was a response', () => {
const expectedUrl = 'https://js.adscale.de/pbsync.html';
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, ['']);
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
it('should not perform user connect when there was no response', () => {
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, []);
assert.deepStrictEqual(userSyncResponse, []);
});
describe('and gdpr consent is defined', () => {
describe('and gdpr applies', () => {
it('should place gdpr query param to the user sync url with value of 1', () => {
const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=';
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true});
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
});
describe('and gdpr does not apply', () => {
it('should place gdpr query param to the user sync url with zero value', () => {
const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent=';
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: false});
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
describe('because consent does not specify it', () => {
it('should place gdpr query param to the user sync url with zero value', () => {
const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent=';
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {});
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
});
});
describe('and consent string is defined', () => {
it('should pass consent string to gdpr consent query param', () => {
const consentString = 'consent_string';
const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=${consentString}`;
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString});
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
it('should correctly escape invalid characters', () => {
const consentString = 'consent ?stri&ng';
const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=consent%20%3Fstri%26ng`;
const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString});
assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]);
});
});
});
});
describe('when iframe option is disabled', () => {
it('should not perform user connect even when there was a response', () => {
const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, ['']);
assert.deepStrictEqual(userSyncResponse, []);
});
it('should not perform user connect when there was no response', () => {
const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, []);
assert.deepStrictEqual(userSyncResponse, []);
});
});
});
});