From 1ba68883a4aa0b6cc6a59e75d1ba07027589e4a0 Mon Sep 17 00:00:00 2001 From: Eddy Pechuzal <46331062+epechuzal@users.noreply.github.com> Date: Tue, 7 Sep 2021 13:57:53 -0700 Subject: [PATCH] Sharethrough adapter: connect to OpenRTB endpoint (#7290) --- modules/sharethroughBidAdapter.js | 427 ++++---- modules/sharethroughBidAdapter.md | 68 +- .../modules/sharethroughBidAdapter_spec.js | 980 ++++++++---------- 3 files changed, 704 insertions(+), 771 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 967d8194607..24ab2530014 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,123 +1,190 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { createEidsArray } from './userId/eids.js'; -const VERSION = '3.4.1'; +const VERSION = '4.0.0'; const BIDDER_CODE = 'sharethrough'; -const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; -const DEFAULT_SIZE = [1, 1]; +const SUPPLY_ID = 'WYu2BXv1'; + +const STR_ENDPOINT = `https://btlr.sharethrough.com/universal/v1?supply_id=${SUPPLY_ID}`; // this allows stubbing of utility function that is used internally by the sharethrough adapter export const sharethroughInternal = { - b64EncodeUnicode, - handleIframe, - isLockedInFrame, - getProtocol + getProtocol, }; export const sharethroughAdapterSpec = { code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { - return bidRequests.map(bidRequest => { - let query = { - placement_key: bidRequest.params.pkey, - bidId: bidRequest.bidId, - consent_required: false, - instant_play_capable: canAutoPlayHTML5Video(), - hbSource: 'prebid', - hbVersion: '$prebid.version$', - strVersion: VERSION, - }; - - const gpid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); - if (gpid) { - query.gpid = gpid; - } - - Object.assign(query, handleUniversalIds(bidRequest)); - - const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; - query.secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { - query.consent_string = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest && bidderRequest.gdprConsent) { - query.consent_required = !!bidderRequest.gdprConsent.gdprApplies; - } - - if (bidderRequest && bidderRequest.uspConsent) { - query.us_privacy = bidderRequest.uspConsent + const timeout = config.getConfig('bidderTimeout'); + + const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; + const secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); + + const req = { + id: utils.generateUUID(), + at: 1, + cur: ['USD'], + tmax: timeout, + site: { + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null, + }, + user: { + ext: { + eids: userIdAsEids(bidRequests[0]), + }, + }, + device: { + ua: navigator.userAgent, + language: navigator.language, + js: 1, + dnt: navigator.doNotTrack === '1' ? 1 : 0, + h: window.screen.height, + w: window.screen.width, + }, + regs: { + coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: {}, + }, + source: { + ext: { + version: '$prebid.version$', + str: VERSION, + schain: bidRequests[0].schain, + }, + }, + bcat: bidRequests[0].params.bcat || [], + badv: bidRequests[0].params.badv || [], + test: 0, + }; + + if (bidderRequest.gdprConsent) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies === true; + req.regs.ext.gdpr = gdprApplies ? 1 : 0; + if (gdprApplies) { + req.user.ext.consent = bidderRequest.gdprConsent.consentString; } + } - if (config.getConfig('coppa') === true) { - query.coppa = true - } + if (bidderRequest.uspConsent) { + req.regs.ext.us_privacy = bidderRequest.uspConsent; + } - if (bidRequest.schain) { - query.schain = JSON.stringify(bidRequest.schain); - } + const imps = bidRequests.map(bidReq => { + const impression = {}; - const floor = getFloor(bidRequest); - if (floor) { - query.bidfloor = floor; + const gpid = utils.deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + impression.ext = { gpid: gpid }; } - if (bidRequest.params.badv) { - query.badv = bidRequest.params.badv; + // if request is for video, we only support instream + if (bidReq.mediaTypes && bidReq.mediaTypes.video && bidReq.mediaTypes.video.context === 'outstream') { + // return null so we can easily remove this imp from the array of imps that we send to adserver + return null; } - if (bidRequest.params.bcat) { - query.bcat = bidRequest.params.bcat; + if (bidReq.mediaTypes && bidReq.mediaTypes.video) { + const videoRequest = bidReq.mediaTypes.video; + + // default playerSize, only change this if we know width and height are properly defined in the request + let [w, h] = [640, 360]; + if (videoRequest.playerSize && videoRequest.playerSize[0] && videoRequest.playerSize[1]) { + [w, h] = videoRequest.playerSize; + } + + impression.video = { + pos: nullish(videoRequest.pos, 0), + topframe: utils.inIframe() ? 0 : 1, + skip: nullish(videoRequest.skip, 0), + linearity: nullish(videoRequest.linearity, 1), + minduration: nullish(videoRequest.minduration, 5), + maxduration: nullish(videoRequest.maxduration, 60), + playbackmethod: videoRequest.playbackmethod || [2], + api: getVideoApi(videoRequest), + mimes: videoRequest.mimes || ['video/mp4'], + protocols: getVideoProtocols(videoRequest), + w, + h, + startdelay: nullish(videoRequest.startdelay, 0), + skipmin: nullish(videoRequest.skipmin, 0), + skipafter: nullish(videoRequest.skipafter, 0), + }; + + if (videoRequest.placement) impression.video.placement = videoRequest.placement; + if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; + if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; + if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; + } else { + impression.banner = { + pos: utils.deepAccess(bidReq, 'mediaTypes.banner.pos', 0), + topframe: utils.inIframe() ? 0 : 1, + format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })), + }; } - // Data that does not need to go to the server, - // but we need as part of interpretResponse() - const strData = { - skipIframeBusting: bidRequest.params.iframe, - iframeSize: bidRequest.params.iframeSize, - sizes: bidRequest.sizes - }; - return { - method: 'POST', - url: STR_ENDPOINT, - data: query, - strData: strData + id: bidReq.bidId, + tagid: String(bidReq.params.pkey), + secure: secure ? 1 : 0, + bidfloor: getBidRequestFloor(bidReq), + ...impression, }; - }) + }).filter(imp => !!imp); + + return { + method: 'POST', + url: STR_ENDPOINT, + data: { + ...req, + imp: imps, + }, + bidRequests, + bidderRequest, + }; }, interpretResponse: ({ body }, req) => { - if (!body || !body.creatives || !body.creatives.length) { + if (!body || !body.seatbid || body.seatbid.length === 0 || !body.seatbid[0].bid || body.seatbid[0].bid.length === 0) { return []; } - const creative = body.creatives[0]; - let size = DEFAULT_SIZE; - if (req.strData.iframeSize || req.strData.sizes.length) { - size = req.strData.iframeSize - ? req.strData.iframeSize - : getLargestSize(req.strData.sizes); - } + return body.seatbid[0].bid.map(bid => { + const request = matchRequest(bid.impid, req); + + const response = { + requestId: bid.impid, + width: +bid.w, + height: +bid.h, + cpm: +bid.price, + creativeId: bid.crid, + dealId: bid.dealid || null, + mediaType: request.mediaTypes && request.mediaTypes.video ? VIDEO : BANNER, + currency: body.cur || 'USD', + netRevenue: true, + ttl: 360, + ad: bid.adm, + nurl: bid.nurl, + meta: { + advertiserDomains: bid.adomain || [], + }, + }; - return [{ - requestId: req.data.bidId, - width: size[0], - height: size[1], - cpm: creative.cpm, - creativeId: creative.creative.creative_key, - dealId: creative.creative.deal_id, - currency: 'USD', - netRevenue: true, - ttl: 360, - meta: { advertiserDomains: creative.creative && creative.creative.adomain ? creative.creative.adomain : [] }, - ad: generateAd(body, req) - }]; + if (response.mediaType === VIDEO) { + response.ttl = 3600; + response.vastXml = bid.adm; + } + + return response; + }); }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { @@ -138,175 +205,77 @@ export const sharethroughAdapterSpec = { }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => {}, + onTimeout: (data) => { + }, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => {}, + onBidWon: (bid) => { + }, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => {} + onSetTargeting: (bid) => { + }, }; -function handleUniversalIds(bidRequest) { - if (!bidRequest.userId) return {}; - - const universalIds = {}; - - const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); - if (ttd) universalIds.ttduid = ttd; - - const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); - if (pubc) universalIds.pubcid = pubc; - - const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); - if (idl) universalIds.idluid = idl; - - const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); - if (id5) { - universalIds.id5uid = { id: id5 }; - const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); - if (id5link) universalIds.id5uid.linkType = id5link; - } - - const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); - if (lipb) universalIds.liuid = lipb; - - return universalIds; -} - -function getLargestSize(sizes) { - function area(size) { - return size[0] * size[1]; +function getVideoApi({ api }) { + let defaultValue = [2]; + if (api && Array.isArray(api) && api.length > 0) { + return api; + } else { + return defaultValue; } - - return sizes.reduce((prev, current) => { - if (area(current) > area(prev)) { - return current - } else { - return prev - } - }); } -function generateAd(body, req) { - const strRespId = `str_response_${req.data.bidId}`; - - let adMarkup = ` -
-
- - `; - - if (req.strData.skipIframeBusting) { - // Don't break out of iframe - adMarkup = adMarkup + ``; +function getVideoProtocols({ protocols }) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + if (protocols && Array.isArray(protocols) && protocols.length > 0) { + return protocols; } else { - // Add logic to the markup that detects whether or not in top level document is accessible - // this logic will deploy sfp.js and/or iframe buster script(s) as appropriate - adMarkup = adMarkup + ` - - `; + return defaultValue; } - - return adMarkup; } -function handleIframe () { - // only load iframe buster JS if we can access the top level document - // if we are 'locked in' to this frame then no point trying to bust out: we may as well render in the frame instead - var iframeBusterLoaded = false; - if (!window.lockedInFrame) { - var sfpIframeBusterJs = document.createElement('script'); - sfpIframeBusterJs.src = 'https://native.sharethrough.com/assets/sfp-set-targeting.js'; - sfpIframeBusterJs.type = 'text/javascript'; - try { - window.document.getElementsByTagName('body')[0].appendChild(sfpIframeBusterJs); - iframeBusterLoaded = true; - } catch (e) { - utils.logError('Trouble writing frame buster script, error details:', e); - } - } - - var clientJsLoaded = (!iframeBusterLoaded) ? !!(window.STR && window.STR.Tag) : !!(window.top.STR && window.top.STR.Tag); - if (!clientJsLoaded) { - var sfpJs = document.createElement('script'); - sfpJs.src = 'https://native.sharethrough.com/assets/sfp.js'; - sfpJs.type = 'text/javascript'; - - // only add sfp js to window.top if iframe busting successfully loaded; otherwise, add to iframe - try { - if (iframeBusterLoaded) { - window.top.document.getElementsByTagName('body')[0].appendChild(sfpJs); - } else { - window.document.getElementsByTagName('body')[0].appendChild(sfpJs); - } - } catch (e) { - utils.logError('Trouble writing sfp script, error details:', e); +function getBidRequestFloor(bid) { + let floor = null; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', + size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), + }); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); } } + return floor !== null ? floor : bid.params.floor; } -// determines if we are capable of busting out of the iframe we are in -// if we catch a DOMException when trying to access top-level document, it means we're stuck in the frame we're in -function isLockedInFrame () { - window.lockedInFrame = false; - try { - window.lockedInFrame = !window.top.document; - } catch (e) { - window.lockedInFrame = (e instanceof DOMException); +function userIdAsEids(bidRequest) { + const eids = createEidsArray(utils.deepAccess(bidRequest, 'userId')) || []; + + const flocData = utils.deepAccess(bidRequest, 'userId.flocId'); + const isFlocIdValid = flocData && flocData.id && flocData.version; + if (isFlocIdValid) { + eids.push({ + source: 'chrome.com', + uids: [{ id: flocData.id, atype: 1, ext: { ver: flocData.version } }], + }); } -} -// See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem -function b64EncodeUnicode(str) { - return btoa( - encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, - function toSolidBytes(match, p1) { - return String.fromCharCode('0x' + p1); - })); + return eids; } -function canAutoPlayHTML5Video() { - const userAgent = navigator.userAgent; - if (!userAgent) return false; - - const isAndroid = /Android/i.test(userAgent); - const isiOS = /iPhone|iPad|iPod/i.test(userAgent); - const chromeVersion = parseInt((/Chrome\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - const chromeiOSVersion = parseInt((/CriOS\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - const safariVersion = parseInt((/Version\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - - if ( - (isAndroid && chromeVersion >= 53) || - (isiOS && (safariVersion >= 10 || chromeiOSVersion >= 53)) || - !(isAndroid || isiOS) - ) { - return true; - } else { - return false; - } +function getProtocol() { + return window.location.protocol; } -function getProtocol() { - return document.location.protocol; +function matchRequest(id, request) { + return request.bidRequests.find(bid => bid.bidId === id); } -function getFloor(bid) { - if (utils.isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: 'banner', - size: bid.sizes.map(size => ({ w: size[0], h: size[1] })) - }); - if (utils.isPlainObject(floorInfo) && !isNaN(floorInfo.floor) && floorInfo.currency === 'USD') { - return parseFloat(floorInfo.floor); - } - } - return null; +// stub for ?? operator +function nullish(input, def) { + return input === null || input === undefined ? def : input; } registerBidder(sharethroughAdapterSpec); diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md index 396b8164577..218ef353d4e 100644 --- a/modules/sharethroughBidAdapter.md +++ b/modules/sharethroughBidAdapter.md @@ -23,22 +23,74 @@ Module that connects to Sharethrough's demand sources // REQUIRED - The placement key pkey: 'LuB3vxGGFrBZJa6tifXW4xgK', - // OPTIONAL - Render Sharethrough creative in an iframe, defaults to false - iframe: true, - - // OPTIONAL - If iframeSize is provided, we'll use this size for the iframe - // otherwise we'll grab the largest size from the sizes array - // This is ignored if iframe: false - iframeSize: [250, 250], - // OPTIONAL - Blocked Advertiser Domains badv: ['domain1.com', 'domain2.com'], // OPTIONAL - Blocked Categories (IAB codes) bcat: ['IAB1-1', 'IAB1-2'], + + // OPTIONAL - default bid floor, if not specified in bid request (USD) + floor: 0.1, } } ] } ]; ``` + +# Sample Instream Video Ad Unit: For Publishers +``` +var adVideoAdUnits = [ +{ + code: 'test-div-video', + mediaTypes: { + video: { + // CANNOT be 'outstream' + context: 'instream', + placement: 1, + delivery: 1, + companiontype: 'companion type', + companionad: 'companion ad', + // default values shown below this point + pos: 0, + skip: 0, + linearity: 1, + minduration: 5, + maxduration: 60, + playbackmethod: [2], + api: [2], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6, 7, 8], + playerSize: [640, 360], + startdelay: 0, + skipmin: 0, + skipafter: 0, + }, + }, + bids: [{ + bidder: 'sharethrough', + params: { + pkey: 'pkey1' + } + }] +}] +``` + +# Sample Banner Ad Unit: For Publishers +``` +var adUnits = [ +{ + code: 'test-div-video', + mediaTypes: { + banner: { + pos: 0, // default + }, + }, + bids: [{ + bidder: 'sharethrough', + params: { + pkey: 'pkey1' + } + }] +}] +``` diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 3e406e1af44..122c6cdbb39 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,247 +1,38 @@ import { expect } from 'chai'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as utils from '../../../src/utils.js'; import { config } from 'src/config'; +import * as utils from 'src/utils'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); -const bidRequests = [ - { - bidder: 'sharethrough', - bidId: 'bidId1', - sizes: [[600, 300]], - placementCode: 'foo', - params: { - pkey: 'aaaa1111' - }, - ortb2Imp: { - ext: { - data: { - pbadslot: 'adslot-id-1' - } - } - }, - userId: { - tdid: 'fake-tdid', - pubcid: 'fake-pubcid', - idl_env: 'fake-identity-link', - id5id: { - uid: 'fake-id5id', - ext: { - linkType: 2 - } - }, - lipb: { - lipbid: 'fake-lipbid' - } - }, - crumbs: { - pubcid: 'fake-pubcid-in-crumbs-obj' - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId2', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'bbbb2222', - iframe: true - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId3', - sizes: [[700, 400]], - placementCode: 'coconut', - params: { - pkey: 'cccc3333', - iframe: true, - iframeSize: [500, 500] - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId4', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'dddd4444', - badv: ['domain1.com', 'domain2.com'] - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId5', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'eeee5555', - bcat: ['IAB1-1', 'IAB1-2'] - } - }, -]; - -const prebidRequests = [ - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: true, - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: true, - iframeSize: [500, 500], - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [[0, 0]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - } -]; - -const bidderResponse = { - body: { - 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', - 'bidId': 'bidId1', - 'version': 1, - 'creatives': [{ - 'auctionWinId': 'b2882d5e-bf8b-44da-a91c-0c11287b8051', - 'cpm': 12.34, - 'creative': { - 'deal_id': 'aDealId', - 'creative_key': 'aCreativeId', - 'title': '✓ à la mode' - } - }], - 'stxUserId': '' - }, - header: { get: (header) => header } -}; - -const setUserAgent = (uaString) => { - window.navigator['__defineGetter__']('userAgent', function() { - return uaString; - }); -}; - -describe('sharethrough internal spec', function() { - let windowStub, windowTopStub; - let stubbedReturn = [{ - appendChild: () => undefined - }] - beforeEach(function() { - windowStub = sinon.stub(window.document, 'getElementsByTagName'); - windowTopStub = sinon.stub(window.top.document, 'getElementsByTagName'); - windowStub.withArgs('body').returns(stubbedReturn); - windowTopStub.withArgs('body').returns(stubbedReturn); - }); - - afterEach(function() { - windowStub.restore(); - windowTopStub.restore(); - window.STR = undefined; - window.top.STR = undefined; - }); - - describe('we cannot access top level document', function() { - beforeEach(function() { - window.lockedInFrame = true; - }); - - afterEach(function() { - window.lockedInFrame = false; - }); - it('appends sfp.js to the safeframe', function() { - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - }); +describe('sharethrough adapter spec', function() { + let protocolStub; + let inIframeStub; - it('does not append anything if sfp.js is already loaded in the safeframe', function() { - window.STR = { Tag: true }; - sharethroughInternal.handleIframe(); - expect(windowStub.notCalled).to.be.true; - expect(windowTopStub.notCalled).to.be.true; - }); + beforeEach(() => { + protocolStub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https'); + inIframeStub = sinon.stub(utils, 'inIframe').returns(false); }); - describe('we are able to bust out of the iframe', function() { - it('appends sfp.js to window.top', function() { - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - expect(windowTopStub.calledOnce).to.be.true; - }); - - it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function() { - window.top.STR = { Tag: true }; - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - expect(windowTopStub.notCalled).to.be.true; - }); + afterEach(() => { + protocolStub.restore(); + inIframeStub.restore(); }); -}); -describe('sharethrough adapter spec', function() { - describe('.code', function() { + describe('code', function() { it('should return a bidder code of sharethrough', function() { expect(spec.code).to.eql('sharethrough'); }); }); - describe('.isBidRequestValid', function() { + describe('isBidRequestValid', function() { it('should return false if req has no pkey', function() { const invalidBidRequest = { bidder: 'sharethrough', params: { - notPKey: 'abc123' - } + notPKey: 'abc123', + }, }; expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); @@ -250,383 +41,504 @@ describe('sharethrough adapter spec', function() { const invalidBidRequest = { bidder: 'notSharethrough', params: { - notPKey: 'abc123' + pkey: 'abc123', } }; expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); it('should return true if req is correct', function() { - expect(spec.isBidRequestValid(bidRequests[0])).to.eq(true); - expect(spec.isBidRequestValid(bidRequests[1])).to.eq(true); + const validBidRequest = { + bidder: 'sharethrough', + params: { + pkey: 'abc123', + }, + }; + expect(spec.isBidRequestValid(validBidRequest)).to.eq(true); }); }); - describe('.buildRequests', function() { - it('should return an array of requests', function() { - const builtBidRequests = spec.buildRequests(bidRequests); + describe('open rtb', () => { + let bidRequests, bidderRequest; + + beforeEach(() => { + config.setConfig({ + bidderTimeout: 242, + coppa: true, + }); - expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[1].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[0].method).to.eq('POST'); + bidRequests = [ + { + bidder: 'sharethrough', + bidId: 'bidId1', + sizes: [[300, 250], [300, 600]], + params: { + pkey: 'aaaa1111', + bcat: ['cat1', 'cat2'], + badv: ['adv1', 'adv2'], + }, + mediaTypes: { + banner: { + pos: 1, + }, + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'universal-id', + }, + }, + }, + userId: { + tdid: 'fake-tdid', + pubcid: 'fake-pubcid', + idl_env: 'fake-identity-link', + id5id: { + uid: 'fake-id5id', + ext: { + linkType: 2, + }, + }, + lipb: { + lipbid: 'fake-lipbid', + }, + criteoId: 'fake-criteo', + britepoolid: 'fake-britepool', + intentIqId: 'fake-intentiq', + lotamePanoramaId: 'fake-lotame', + parrableId: { + eid: 'fake-parrable', + }, + netId: 'fake-netid', + sharedid: { + id: 'fake-sharedid', + }, + flocId: { + id: 'fake-flocid', + version: '42', + }, + }, + crumbs: { + pubcid: 'fake-pubcid-in-crumbs-obj', + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1, + }, + ], + }, + getFloor: () => ({ currency: 'USD', floor: 42 }), + }, + { + bidder: 'sharethrough', + bidId: 'bidId2', + sizes: [[600, 300]], + params: { + pkey: 'bbbb2222', + }, + mediaTypes: { + video: { + pos: 3, + skip: 1, + linearity: 0, + minduration: 10, + maxduration: 30, + playbackmethod: [1], + api: [3], + mimes: ['video/3gpp'], + protocols: [2, 3], + playerSize: [640, 480], + startdelay: 42, + skipmin: 10, + skipafter: 20, + placement: 1, + delivery: 1, + companiontype: 'companion type', + companionad: 'companion ad', + context: 'instream', + }, + }, + getFloor: () => ({ currency: 'USD', floor: 42 }), + }, + ]; + + bidderRequest = { + refererInfo: { + referer: 'https://referer.com', + }, + }; }); - it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { - setUserAgent('Android Chrome/60'); - let builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; + describe('buildRequests', function() { + describe('top level object', () => { + it('should build openRTB request', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + + expect(builtRequest.method).to.equal('POST'); + expect(builtRequest.url).not.to.be.undefined; + expect(builtRequest.options).to.be.undefined; + expect(builtRequest.bidderRequest).to.deep.equal(bidderRequest); + + const openRtbReq = builtRequest.data; + expect(openRtbReq.id).not.to.be.undefined; + expect(openRtbReq.cur).to.deep.equal(['USD']); + expect(openRtbReq.tmax).to.equal(242); + + expect(openRtbReq.site.domain).not.to.be.undefined; + expect(openRtbReq.site.page).not.to.be.undefined; + expect(openRtbReq.site.ref).to.equal('https://referer.com'); + + const expectedEids = { + 'liveramp.com': { id: 'fake-identity-link' }, + 'id5-sync.com': { id: 'fake-id5id' }, + 'pubcid.org': { id: 'fake-pubcid' }, + 'adserver.org': { id: 'fake-tdid' }, + 'criteo.com': { id: 'fake-criteo' }, + 'britepool.com': { id: 'fake-britepool' }, + 'liveintent.com': { id: 'fake-lipbid' }, + 'intentiq.com': { id: 'fake-intentiq' }, + 'crwdcntrl.net': { id: 'fake-lotame' }, + 'parrable.com': { id: 'fake-parrable' }, + 'netid.de': { id: 'fake-netid' }, + 'chrome.com': { id: 'fake-flocid' }, + }; + expect(openRtbReq.user.ext.eids).to.be.an('array').that.have.length(Object.keys(expectedEids).length); + for (const eid of openRtbReq.user.ext.eids) { + expect(Object.keys(expectedEids)).to.include(eid.source); + expect(eid.uids[0].id).to.equal(expectedEids[eid.source].id); + expect(eid.uids[0].atype).to.be.ok; + } - setUserAgent('iPhone Version/11'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; + expect(openRtbReq.device.ua).to.equal(navigator.userAgent); + expect(openRtbReq.regs.coppa).to.equal(1); - setUserAgent('iPhone CriOS/60'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; + expect(openRtbReq.source.ext.version).not.to.be.undefined; + expect(openRtbReq.source.ext.str).not.to.be.undefined; + expect(openRtbReq.source.ext.schain).to.deep.equal(bidRequests[0].schain); - setUserAgent('Android Chrome/50'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; + expect(openRtbReq.bcat).to.deep.equal(bidRequests[0].params.bcat); + expect(openRtbReq.badv).to.deep.equal(bidRequests[0].params.badv); - setUserAgent('Android Chrome'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; + expect(openRtbReq.imp).to.have.length(2); - setUserAgent(undefined); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; - }); + expect(openRtbReq.imp[0].id).to.equal('bidId1'); + expect(openRtbReq.imp[0].tagid).to.equal('aaaa1111'); + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[0].bidfloor).to.equal(42); - it('should set the secure parameter to false when the protocol is http', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('http:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.false; - stub.restore(); - }); + expect(openRtbReq.imp[1].id).to.equal('bidId2'); + expect(openRtbReq.imp[1].tagid).to.equal('bbbb2222'); + expect(openRtbReq.imp[1].secure).to.equal(1); + expect(openRtbReq.imp[1].bidfloor).to.equal(42); + }); - it('should set the secure parameter to true when the protocol is https', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.true; - stub.restore(); - }); + it('should have empty eid array if no id is provided', () => { + const openRtbReq = spec.buildRequests([bidRequests[1]], bidderRequest).data; - it('should set the secure parameter to true when the protocol is neither http or https', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('about:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.true; - stub.restore(); - }); + expect(openRtbReq.user.ext.eids).to.deep.equal([]); + }); + }); - it('should add ccpa parameter if uspConsent is present', function() { - const uspConsent = '1YNN'; - const bidderRequest = { uspConsent: uspConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data.us_privacy).to.eq(uspConsent); - }); + describe('regulation', () => { + describe('gdpr', () => { + it('should populate request accordingly when gdpr applies', () => { + bidderRequest.gdprConsent = { + gdprApplies: true, + consentString: 'consent', + }; - it('should add consent parameters if gdprConsent is present', function() { - const gdprConsent = { consentString: 'consent_string123', gdprApplies: true }; - const bidderRequest = { gdprConsent: gdprConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data.consent_required).to.eq(true); - expect(bidRequest.data.consent_string).to.eq('consent_string123'); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should handle gdprConsent is present but values are undefined case', function() { - const gdprConsent = { consent_string: undefined, gdprApplies: undefined }; - const bidderRequest = { gdprConsent: gdprConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data).to.not.include.any.keys('consent_string'); - }); + expect(openRtbReq.regs.ext.gdpr).to.equal(1); + expect(openRtbReq.user.ext.consent).to.equal('consent'); + }); - it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.ttduid).to.eq('fake-tdid'); - }); + it('should populate request accordingly when gdpr explicitly does not apply', () => { + bidderRequest.gdprConsent = { + gdprApplies: false, + }; - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' userId object of the bidrequest', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' crumbs object of the bidrequest', function() { - const bidData = utils.deepClone(bidRequests); - delete bidData[0].userId.pubcid; + expect(openRtbReq.regs.ext.gdpr).to.equal(0); + expect(openRtbReq.user.ext.consent).to.be.undefined; + }); + }); - const bidRequest = spec.buildRequests(bidData)[0]; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid-in-crumbs-obj'); - }); + describe('US privacy', () => { + it('should populate request accordingly when us privacy applies', () => { + bidderRequest.uspConsent = 'consent'; - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' crumbs object of the bidrequest', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - delete bidRequest.userId; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should add the idluid parameter if a bid request contains a value for Identity Link from Live Ramp', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.idluid).to.eq('fake-identity-link'); - }); + expect(openRtbReq.regs.ext.us_privacy).to.equal('consent'); + }); + }); - it('should add the id5uid parameter if a bid request contains a value for ID5', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.id5uid.id).to.eq('fake-id5id'); - expect(bidRequest.data.id5uid.linkType).to.eq(2); - }); + describe('coppa', () => { + it('should populate request accordingly when coppa does not apply', () => { + config.setConfig({ coppa: false }); - it('should add the liuid parameter if a bid request contains a value for LiveIntent ID', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.liuid).to.eq('fake-lipbid'); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should add Sharethrough specific parameters', function() { - const builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0]).to.deep.include({ - strData: { - skipIframeBusting: undefined, - iframeSize: undefined, - sizes: [[600, 300]] - } + expect(openRtbReq.regs.coppa).to.equal(0); + }); + }); }); - }); - - it('should add a supply chain parameter if schain is present', function() { - // shallow copy of the first bidRequest obj, so we don't mutate - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest['schain'] = { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'directseller.com', - sid: '00001', - rid: 'BidRequest1', - hp: 1 - } - ] - }; - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.schain).to.eq(JSON.stringify(bidRequest.schain)); - }); - - describe('gpid', () => { - it('should include the gpid param if pbadslot is found in ortb2Imp in the bid request', () => { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.gpid).to.eq('adslot-id-1') - }); + describe('universal id', () => { + it('should include gpid when universal id is provided', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should not include the gpid param if pbadslot is not found in ortb2Imp in the bid request', () => { - const bidRequest = spec.buildRequests(bidRequests)[1]; - expect(bidRequest.data).to.not.include.any.keys('gpid'); + expect(openRtbReq.imp[0].ext.gpid).to.equal('universal-id'); + expect(openRtbReq.imp[1].ext).to.be.undefined; + }); }); - }); - - it('should add badv if provided', () => { - const builtBidRequest = spec.buildRequests([bidRequests[3]])[0]; - - expect(builtBidRequest.data.badv).to.have.members(['domain1.com', 'domain2.com']) - }); - - it('should add bcat if provided', () => { - const builtBidRequest = spec.buildRequests([bidRequests[4]])[0]; - expect(builtBidRequest.data.bcat).to.have.members(['IAB1-1', 'IAB1-2']) - }); + describe('secure flag', () => { + it('should be positive when protocol is https', () => { + protocolStub.returns('https'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should not add a supply chain parameter if schain is missing', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data).to.not.include.any.keys('schain'); - }); + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[1].secure).to.equal(1); + }); - it('should include the bidfloor parameter if it is present in the bid request', function() { - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest['getFloor'] = () => ({ currency: 'USD', floor: 0.5 }); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.bidfloor).to.eq(0.5); - }); + it('should be negative when protocol is http', () => { + protocolStub.returns('http'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should not include the bidfloor parameter if it is missing in the bid request', function() { - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data).to.not.include.any.keys('bidfloor'); - }); + expect(openRtbReq.imp[0].secure).to.equal(0); + expect(openRtbReq.imp[1].secure).to.equal(0); + }); - describe('coppa', function() { - it('should add coppa to request if enabled', function() { - config.setConfig({coppa: true}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.eq(true); - }); + it('should be positive when protocol is neither http nor https', () => { + protocolStub.returns('about'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should not add coppa to request if disabled', function() { - config.setConfig({coppa: false}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.be.undefined; + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[1].secure).to.equal(1); + }); }); - it('should not add coppa to request if unknown value', function() { - config.setConfig({coppa: 'something'}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.be.undefined; - }); - }); - }); + describe('banner imp', () => { + it('should generate open rtb banner imp', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); - describe('.interpretResponse', function() { - it('returns a correctly parsed out response', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.deep.include( - { - width: 1, - height: 1, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360, - meta: { advertiserDomains: [] } + const bannerImp = builtRequest.data.imp[0].banner; + expect(bannerImp.pos).to.equal(1); + expect(bannerImp.topframe).to.equal(1); + expect(bannerImp.format).to.deep.equal([{ w: 300, h: 250 }, { w: 300, h: 600 }]); }); - }); - it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( - { - width: 300, - height: 300, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 - }); - }); + it('should default to pos 0 if not provided', () => { + delete bidRequests[0].mediaTypes; + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include( - { - width: 500, - height: 500, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + const bannerImp = builtRequest.data.imp[0].banner; + expect(bannerImp.pos).to.equal(0); }); - }); + }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include( - { - width: 0, - height: 0, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + describe('video imp', () => { + it('should generate open rtb video imp', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + + const videoImp = builtRequest.data.imp[1].video; + expect(videoImp.pos).to.equal(3); + expect(videoImp.topframe).to.equal(1); + expect(videoImp.skip).to.equal(1); + expect(videoImp.linearity).to.equal(0); + expect(videoImp.minduration).to.equal(10); + expect(videoImp.maxduration).to.equal(30); + expect(videoImp.playbackmethod).to.deep.equal([1]); + expect(videoImp.api).to.deep.equal([3]); + expect(videoImp.mimes).to.deep.equal(['video/3gpp']); + expect(videoImp.protocols).to.deep.equal([2, 3]); + expect(videoImp.w).to.equal(640); + expect(videoImp.h).to.equal(480); + expect(videoImp.startdelay).to.equal(42); + expect(videoImp.skipmin).to.equal(10); + expect(videoImp.skipafter).to.equal(20); + expect(videoImp.placement).to.equal(1); + expect(videoImp.delivery).to.equal(1); + expect(videoImp.companiontype).to.equal('companion type'); + expect(videoImp.companionad).to.equal('companion ad'); }); - }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include( - { - width: 300, - height: 300, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + it('should set defaults if no value provided', () => { + delete bidRequests[1].mediaTypes.video.pos; + delete bidRequests[1].mediaTypes.video.skip; + delete bidRequests[1].mediaTypes.video.linearity; + delete bidRequests[1].mediaTypes.video.minduration; + delete bidRequests[1].mediaTypes.video.maxduration; + delete bidRequests[1].mediaTypes.video.playbackmethod; + delete bidRequests[1].mediaTypes.video.api; + delete bidRequests[1].mediaTypes.video.mimes; + delete bidRequests[1].mediaTypes.video.protocols; + delete bidRequests[1].mediaTypes.video.playerSize; + delete bidRequests[1].mediaTypes.video.startdelay; + delete bidRequests[1].mediaTypes.video.skipmin; + delete bidRequests[1].mediaTypes.video.skipafter; + delete bidRequests[1].mediaTypes.video.placement; + delete bidRequests[1].mediaTypes.video.delivery; + delete bidRequests[1].mediaTypes.video.companiontype; + delete bidRequests[1].mediaTypes.video.companionad; + + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + + const videoImp = builtRequest.data.imp[1].video; + expect(videoImp.pos).to.equal(0); + expect(videoImp.skip).to.equal(0); + expect(videoImp.linearity).to.equal(1); + expect(videoImp.minduration).to.equal(5); + expect(videoImp.maxduration).to.equal(60); + expect(videoImp.playbackmethod).to.deep.equal([2]); + expect(videoImp.api).to.deep.equal([2]); + expect(videoImp.mimes).to.deep.equal(['video/mp4']); + expect(videoImp.protocols).to.deep.equal([2, 3, 5, 6, 7, 8]); + expect(videoImp.w).to.equal(640); + expect(videoImp.h).to.equal(360); + expect(videoImp.startdelay).to.equal(0); + expect(videoImp.skipmin).to.equal(0); + expect(videoImp.skipafter).to.equal(0); + expect(videoImp.placement).to.be.undefined; + expect(videoImp.delivery).to.be.undefined; + expect(videoImp.companiontype).to.be.undefined; + expect(videoImp.companionad).to.be.undefined; }); - }); - it('returns a blank array if there are no creatives', function() { - const bidResponse = { body: { creatives: [] } }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; - }); + it('should not return a video impression if context is outstream', () => { + bidRequests[1].mediaTypes.video.context = 'outstream'; + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); - it('returns a blank array if body object is empty', function() { - const bidResponse = { body: {} }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; - }); - - it('returns a blank array if body is null', function() { - const bidResponse = { body: null }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; + const videoImp = builtRequest.data.imp[1]; + expect(videoImp).to.be.undefined; + }); + }); }); - it('correctly generates ad markup when skipIframeBusting is false', function() { - const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; - let resp = null; + describe('interpretResponse', function() { + let request; + let response; + + beforeEach(() => { + request = spec.buildRequests(bidRequests, bidderRequest); + response = { + body: { + seatbid: [{ + bid: [{ + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }], + }], + }, + }; + }); - expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); - expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw(); - expect(adMarkup).to.match( - /data-str-native-key="pKey" data-stx-response-name="str_response_bidId"/); - expect(!!adMarkup.indexOf(resp)).to.eql(true); + describe('banner', () => { + it('should return a banner bid', () => { + const resp = spec.interpretResponse(response, request); + + const bannerBid = resp[0]; + expect(bannerBid.requestId).to.equal('bidId1'); + expect(bannerBid.width).to.equal(300); + expect(bannerBid.height).to.equal(250); + expect(bannerBid.cpm).to.equal(42); + expect(bannerBid.creativeId).to.equal('creative'); + expect(bannerBid.dealId).to.equal('deal'); + expect(bannerBid.mediaType).to.equal('banner'); + expect(bannerBid.currency).to.equal('USD'); + expect(bannerBid.netRevenue).to.equal(true); + expect(bannerBid.ttl).to.equal(360); + expect(bannerBid.ad).to.equal('markup'); + expect(bannerBid.meta.advertiserDomains).to.deep.equal(['domain.com']); + expect(bannerBid.vastXml).to.be.undefined; + }); + }); - // insert functionality to autodetect whether or not in safeframe, and handle JS insertion - expect(adMarkup).to.match(/isLockedInFrame/); - expect(adMarkup).to.match(/handleIframe/); + describe('video', () => { + it('should return a video bid', () => { + const resp = spec.interpretResponse(response, request); + + const bannerBid = resp[1]; + expect(bannerBid.requestId).to.equal('bidId2'); + expect(bannerBid.width).to.equal(640); + expect(bannerBid.height).to.equal(480); + expect(bannerBid.cpm).to.equal(42); + expect(bannerBid.creativeId).to.be.undefined; + expect(bannerBid.dealId).to.be.null; + expect(bannerBid.mediaType).to.equal('video'); + expect(bannerBid.currency).to.equal('USD'); + expect(bannerBid.netRevenue).to.equal(true); + expect(bannerBid.ttl).to.equal(3600); + expect(bannerBid.ad).to.equal('vastTag'); + expect(bannerBid.meta.advertiserDomains).to.deep.equal([]); + expect(bannerBid.vastXml).to.equal('vastTag'); + }); + }); }); - it('correctly generates ad markup when skipIframeBusting is true', function() { - const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; - let resp = null; - - expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); - expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw(); - expect(adMarkup).to.match( - /data-str-native-key="pKey" data-stx-response-name="str_response_bidId"/); - expect(!!adMarkup.indexOf(resp)).to.eql(true); - expect(adMarkup).to.match( - /