UNPKG

mk9-prebid

Version:

Header Bidding Management Library

564 lines (519 loc) 17 kB
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') } }); }); });