mk9-prebid
Version:
Header Bidding Management Library
564 lines (519 loc) • 17 kB
JavaScript
import * as utils from 'src/utils.js';
import { createEidsArray } from 'modules/userId/eids.js';
import { expect } from 'chai';
import { spec } from 'modules/amxBidAdapter.js';
import { BANNER, VIDEO } from 'src/mediaTypes.js';
import { config } from 'src/config.js';
const sampleRequestId = '82c91e127a9b93e';
const sampleDisplayAd = (additionalImpressions) => `<script src='https://assets.a-mo.net/tmode.v1.js'></script>${additionalImpressions}`;
const sampleDisplayCRID = '78827819';
// minimal example vast
const sampleVideoAd = (addlImpression) => `
xml version="1.0" encoding="UTF-8" <VAST version="2.0"><Ad id="128a6.44d74.46b3"><InLine><Error><![CDATA[http://example.net/hbx/verr?e=]]></Error><Impression><![CDATA[http://example.net/hbx/vimp?lid=test&aid=testapp]]></Impression><Creatives><Creative sequence="1"><Linear><Duration>00:00:15</Duration><TrackingEvents><Tracking event="firstQuartile"><![CDATA[https://example.com?event=first_quartile]]></Tracking></TrackingEvents><VideoClicks><ClickThrough><![CDATA[http://example.com]]></ClickThrough></VideoClicks><MediaFiles><MediaFile delivery="progressive" width="16" height="9" type="video/mp4" bitrate="800"><![CDATA[https://example.com/media.mp4]]></MediaFile></MediaFiles></Linear></Creative></Creatives>${addlImpression}</InLine></Ad></VAST>
`.replace(/\n+/g, '')
const embeddedTrackingPixel = `https://1x1.a-mo.net/hbx/g_impression?A=sample&B=20903`;
const sampleNurl = 'https://example.exchange/nurl';
const sampleFPD = {
site: {
keywords: 'sample keywords',
ext: {
data: {
pageType: 'article'
}
}
},
user: {
gender: 'O',
yob: 1982,
}
};
const stubConfig = (withStub) => {
const stub = sinon.stub(config, 'getConfig').callsFake(
(arg) => arg === 'ortb2' ? sampleFPD : null
)
withStub();
stub.restore();
};
const sampleBidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: utils.getUniqueIdentifierStr(),
vendorData: {}
},
auctionId: utils.getUniqueIdentifierStr(),
uspConsent: '1YYY',
refererInfo: {
referer: 'https://www.prebid.org',
canonicalUrl: 'https://www.prebid.org/the/link/to/the/page'
}
};
const sampleBidRequestBase = {
bidder: spec.code,
params: {
endpoint: 'https://httpbin.org/post',
},
sizes: [[320, 50]],
getFloor(params) {
if (params.size == null || params.currency == null || params.mediaType == null) {
throw new Error(`getFloor called with incomplete params: ${JSON.stringify(params)}`)
}
return {
floor: 0.5,
currency: 'USD'
}
},
mediaTypes: {
[BANNER]: {
sizes: [[300, 250]]
}
},
adUnitCode: 'div-gpt-ad-example',
transactionId: utils.getUniqueIdentifierStr(),
bidId: sampleRequestId,
auctionId: utils.getUniqueIdentifierStr(),
};
const schainConfig = {
ver: '1.0',
nodes: [{
asi: 'greatnetwork.exchange',
sid: '000001',
hp: 1,
rid: 'bid_request_1',
domain: 'publisher.com'
}]
};
const sampleBidRequestVideo = {
...sampleBidRequestBase,
bidId: sampleRequestId + '_video',
sizes: [[300, 150]],
schain: schainConfig,
mediaTypes: {
[VIDEO]: {
sizes: [[360, 250]],
context: 'adpod',
adPodDurationSec: 90,
contentMode: 'live'
}
}
};
const sampleServerResponse = {
'p': {
'hreq': ['https://1x1.a-mo.net/hbx/g_sync?partner=test', 'https://1x1.a-mo.net/hbx/g_syncf?__st=iframe']
},
'r': {
[sampleRequestId]: [
{
'b': [
{
'adid': '78827819',
'adm': sampleDisplayAd(''),
'adomain': [
'example.com'
],
'crid': sampleDisplayCRID,
'ext': {
'himp': [
embeddedTrackingPixel
],
},
'nurl': sampleNurl,
'h': 600,
'id': '2014691335735134254',
'impid': '1',
'exp': 90,
'price': 0.25,
'w': 300
},
{
'adid': '222976952',
'adm': sampleVideoAd(''),
'adomain': [
'example.com'
],
'crid': sampleDisplayCRID,
'ext': {
'himp': [
embeddedTrackingPixel
],
},
'nurl': sampleNurl,
'h': 1,
'id': '7735706981389902829',
'impid': '1',
'exp': 90,
'price': 0.25,
'w': 1
},
],
}
]
},
}
describe('AmxBidAdapter', () => {
describe('isBidRequestValid', () => {
it('endpoint must be an optional string', () => {
expect(spec.isBidRequestValid({params: { endpoint: 1 }})).to.equal(false)
expect(spec.isBidRequestValid({params: { endpoint: 'test' }})).to.equal(true)
});
it('tagId is an optional string', () => {
expect(spec.isBidRequestValid({params: { tagId: 1 }})).to.equal(false)
expect(spec.isBidRequestValid({params: { tagId: 'test' }})).to.equal(true)
});
it('testMode is an optional truthy value', () => {
expect(spec.isBidRequestValid({params: { testMode: 1 }})).to.equal(true)
expect(spec.isBidRequestValid({params: { testMode: 'true' }})).to.equal(true)
// ignore invalid values (falsy)
expect(spec.isBidRequestValid({params: { testMode: 'non-truthy-invalid-value' }})).to.equal(true)
expect(spec.isBidRequestValid({params: { testMode: false }})).to.equal(true)
});
it('none of the params are required', () => {
expect(spec.isBidRequestValid({})).to.equal(true)
});
})
describe('getUserSync', () => {
it('will only sync from valid server responses', () => {
const syncs = spec.getUserSyncs({ iframeEnabled: true });
expect(syncs).to.eql([]);
});
it('will return valid syncs from a server response', () => {
const syncs = spec.getUserSyncs({ iframeEnabled: true }, [{body: sampleServerResponse}]);
expect(syncs.length).to.equal(2);
expect(syncs[0].type).to.equal('image');
expect(syncs[1].type).to.equal('iframe');
});
it('will filter out iframe syncs based on options', () => {
const syncs = spec.getUserSyncs({ iframeEnabled: false }, [{body: sampleServerResponse}, {body: sampleServerResponse}]);
expect(syncs.length).to.equal(2);
expect(syncs).to.satisfy((allSyncs) => allSyncs.every((sync) => sync.type === 'image'))
});
});
describe('buildRequests', () => {
it('will default to prebid.a-mo.net endpoint', () => {
const { url } = spec.buildRequests([], sampleBidderRequest);
expect(url).to.equal('https://prebid.a-mo.net/a/c')
});
it('will read the prebid version & global', () => {
const { data: { V: prebidVersion, vg: prebidGlobal } } = spec.buildRequests([{
...sampleBidRequestBase,
params: {
testMode: true
}
}], sampleBidderRequest);
expect(prebidVersion).to.equal('$prebid.version$')
expect(prebidGlobal).to.equal('$$PREBID_GLOBAL$$')
});
it('reads test mode from the first bid request', () => {
const { data } = spec.buildRequests([{
...sampleBidRequestBase,
params: {
testMode: true
}
}], sampleBidderRequest);
expect(data.tm).to.equal(true);
});
it('if prebid is in an iframe, will use the frame url as domain, if the topmost is not avialable', () => {
const { data } = spec.buildRequests([sampleBidRequestBase], {
...sampleBidderRequest,
refererInfo: {
numIframes: 1,
referer: 'http://search-traffic-source.com',
stack: []
}
});
expect(data.do).to.equal('localhost')
expect(data.re).to.equal('http://search-traffic-source.com');
});
it('if we are in AMP, make sure we use the canonical URL or the referrer (which is sourceUrl)', () => {
const { data } = spec.buildRequests([sampleBidRequestBase], {
...sampleBidderRequest,
refererInfo: {
isAmp: true,
referer: 'http://real-publisher-site.com/content',
stack: []
}
});
expect(data.do).to.equal('real-publisher-site.com')
expect(data.re).to.equal('http://real-publisher-site.com/content');
})
it('if prebid is in an iframe, will use the topmost url as domain', () => {
const { data } = spec.buildRequests([sampleBidRequestBase], {
...sampleBidderRequest,
refererInfo: {
numIframes: 1,
referer: 'http://search-traffic-source.com',
stack: ['http://top-site.com', 'http://iframe.com']
}
});
expect(data.do).to.equal('top-site.com');
expect(data.re).to.equal('http://search-traffic-source.com');
});
it('handles referer data and GDPR, USP Consent, COPPA', () => {
const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest);
delete data.m; // don't deal with "m" in this test
expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies)
expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString)
expect(data.usp).to.equal(sampleBidderRequest.uspConsent)
expect(data.cpp).to.equal(0)
});
it('will forward bid request count & wins count data', () => {
const bidderRequestsCount = Math.floor(Math.random() * 100)
const bidderWinsCount = Math.floor(Math.random() * 100)
const { data } = spec.buildRequests([{
...sampleBidRequestBase,
bidderRequestsCount,
bidderWinsCount
}], sampleBidderRequest);
expect(data.brc).to.equal(bidderRequestsCount)
expect(data.bwc).to.equal(bidderWinsCount)
expect(data.trc).to.equal(0)
});
it('will forward first-party data', () => {
stubConfig(() => {
const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest);
expect(data.fpd2).to.deep.equal(sampleFPD)
});
});
it('will collect & forward RTI user IDs', () => {
const randomRTI = `greatRTI${Math.floor(Math.random() * 100)}`
const userId = {
britepoolid: 'sample-britepool',
criteoId: 'sample-criteo',
digitrustid: {data: {id: 'sample-digitrust'}},
id5id: {uid: 'sample-id5'},
idl_env: 'sample-liveramp',
lipb: {lipbid: 'sample-liveintent'},
netId: 'sample-netid',
parrableId: { eid: 'sample-parrable' },
pubcid: 'sample-pubcid',
[randomRTI]: 'sample-unknown',
tdid: 'sample-ttd',
};
const eids = createEidsArray(userId);
const bid = {
...sampleBidRequestBase,
userIdAsEids: eids
};
const { data } = spec.buildRequests([bid, bid], sampleBidderRequest);
expect(data.eids).to.deep.equal(eids)
});
it('can build a banner request', () => {
const { method, url, data } = spec.buildRequests([sampleBidRequestBase, {
...sampleBidRequestBase,
bidId: sampleRequestId + '_2',
params: {
...sampleBidRequestBase.params,
tagId: 'example'
}
}], sampleBidderRequest)
expect(url).to.equal(sampleBidRequestBase.params.endpoint)
expect(method).to.equal('POST');
expect(Object.keys(data.m).length).to.equal(2);
expect(data.m[sampleRequestId]).to.deep.equal({
av: true,
au: 'div-gpt-ad-example',
vd: {},
ms: [
[[320, 50]],
[[300, 250]],
[]
],
aw: 300,
sc: {},
ah: 250,
tf: 0,
f: 0.5,
vr: false
});
expect(data.m[sampleRequestId + '_2']).to.deep.equal({
av: true,
aw: 300,
au: 'div-gpt-ad-example',
sc: {},
ms: [
[[320, 50]],
[[300, 250]],
[]
],
i: 'example',
ah: 250,
vd: {},
tf: 0,
f: 0.5,
vr: false,
});
});
it('can build a video request', () => {
const { data } = spec.buildRequests([sampleBidRequestVideo], sampleBidderRequest);
expect(Object.keys(data.m).length).to.equal(1);
expect(data.m[sampleRequestId + '_video']).to.deep.equal({
au: 'div-gpt-ad-example',
ms: [
[[300, 150]],
[],
[[360, 250]]
],
av: true,
aw: 360,
ah: 250,
sc: schainConfig,
vd: {
sizes: [[360, 250]],
context: 'adpod',
adPodDurationSec: 90,
contentMode: 'live'
},
tf: 0,
f: 0.5,
vr: true
});
});
});
describe('interpretResponse', () => {
const baseBidResponse = {
requestId: sampleRequestId,
cpm: 0.25,
creativeId: sampleDisplayCRID,
currency: 'USD',
netRevenue: true,
meta: {
advertiserDomains: ['example.com'],
},
};
const baseRequest = {
data: {
m: {
[sampleRequestId]: {
aw: 300,
ah: 250,
},
}
}
};
it('will handle a nobid response', () => {
const parsed = spec.interpretResponse({ body: '' }, baseRequest)
expect(parsed).to.eql([])
});
it('can parse a display ad', () => {
const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest)
expect(parsed.length).to.equal(2)
// we should have display, video, display
expect(parsed[0]).to.deep.equal({
...baseBidResponse,
meta: {
...baseBidResponse.meta,
mediaType: BANNER,
},
mediaType: BANNER,
width: 300,
height: 600, // from the bid itself
ttl: 90,
ad: sampleDisplayAd(
`<img src="${embeddedTrackingPixel}" width="0" height="0"/>` +
`<img src="${sampleNurl}" width="0" height="0"/>`
),
});
});
it('can parse a video ad', () => {
const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest)
expect(parsed.length).to.equal(2)
expect(parsed[1]).to.deep.equal({
...baseBidResponse,
meta: {
...baseBidResponse.meta,
mediaType: VIDEO,
},
mediaType: VIDEO,
vastXml: sampleVideoAd(''),
width: 300,
height: 250,
ttl: 90,
});
});
});
describe('analytics methods', () => {
let firedPixels = [];
let _Image = window.Image;
before(() => {
_Image = window.Image;
window.Image = class FakeImage {
set src(value) {
firedPixels.push(value)
}
}
});
beforeEach(() => {
firedPixels = [];
});
after(() => {
window.Image = _Image;
});
it('will fire an event for onSetTargeting', () => {
spec.onSetTargeting({
bidder: 'example',
width: 300,
height: 250,
adId: 'ad-id',
mediaType: BANNER,
cpm: 1.23,
requestId: utils.getUniqueIdentifierStr(),
adUnitCode: 'div-gpt-ad',
adserverTargeting: {
hb_pb: '1.23',
hb_adid: 'ad-id',
hb_bidder: 'example'
}
});
expect(firedPixels.length).to.equal(1)
expect(firedPixels[0]).to.match(/\/hbx\/g_pbst/)
try {
const parsed = new URL(firedPixels[0]);
const nestedData = parsed.searchParams.get('c2');
expect(nestedData).to.equal(utils.formatQS({
hb_pb: '1.23',
hb_adid: 'ad-id',
hb_bidder: 'example'
}));
} catch (e) {
// unsupported browser; try testing for string
const pixel = firedPixels[0];
expect(pixel).to.have.string(encodeURIComponent('hb_pb=1.23'))
expect(pixel).to.have.string(encodeURIComponent('hb_adid=ad-id'))
}
});
it('will log an event for timeout', () => {
spec.onTimeout({
bidder: 'example',
bidId: 'test-bid-id',
adUnitCode: 'div-gpt-ad',
timeout: 300,
auctionId: utils.getUniqueIdentifierStr()
});
expect(firedPixels.length).to.equal(1)
expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/)
});
it('will log an event for prebid win', () => {
spec.onBidWon({
bidder: 'example',
adId: 'test-ad-id',
width: 300,
height: 250,
mediaType: VIDEO,
cpm: 1.34,
adUnitCode: 'div-gpt-ad',
timeout: 300,
auctionId: utils.getUniqueIdentifierStr()
});
expect(firedPixels.length).to.equal(1)
expect(firedPixels[0]).to.match(/\/hbx\/g_pbwin/)
const pixel = firedPixels[0];
try {
const url = new URL(pixel);
expect(url.searchParams.get('C')).to.equal('1')
expect(url.searchParams.get('np')).to.equal('1.34')
} catch (e) {
expect(pixel).to.have.string('C=1')
expect(pixel).to.have.string('np=1.34')
}
});
});
});