From 46666327c07ba4ae8ccb8c4bfe8b5be8fade7bc1 Mon Sep 17 00:00:00 2001 From: rmartinez Date: Wed, 22 Jan 2020 09:30:29 -0800 Subject: [PATCH] Updating the audience network adapter to be 3.x compliant --- modules/audienceNetworkBidAdapter.js | 309 ++++++++++ .../modules/audienceNetworkBidAdapter_spec.js | 568 ++++++++++++++++++ 2 files changed, 877 insertions(+) create mode 100644 modules/audienceNetworkBidAdapter.js create mode 100644 test/spec/modules/audienceNetworkBidAdapter_spec.js diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js new file mode 100644 index 00000000000..04a9cced94c --- /dev/null +++ b/modules/audienceNetworkBidAdapter.js @@ -0,0 +1,309 @@ +/** + * @file AudienceNetwork adapter. + */ +import { registerBidder } from '../src/adapters/bidderFactory'; +import { formatQS } from '../src/url'; +import { generateUUID, deepAccess, convertTypes } from '../src/utils'; +import findIndex from 'core-js/library/fn/array/find-index'; +import includes from 'core-js/library/fn/array/includes'; + +const code = 'audienceNetwork'; +const currency = 'USD'; +const method = 'GET'; +const url = 'https://an.facebook.com/v2/placementbid.json'; +const supportedMediaTypes = ['banner', 'video']; +const netRevenue = true; +const hbBidder = 'fan'; +const ttl = 600; +const videoTtl = 3600; +const platver = '$prebid.version$'; +const platform = '241394079772386'; +const adapterver = '1.3.0'; + +/** + * Does this bid request contain valid parameters? + * @param {Object} bid + * @returns {Boolean} + */ +const isBidRequestValid = bid => + typeof bid.params === 'object' && + typeof bid.params.placementId === 'string' && + bid.params.placementId.length > 0 && + Array.isArray(bid.sizes) && bid.sizes.length > 0 && + (isFullWidth(bid.params.format) ? bid.sizes.map(flattenSize).some(size => size === '300x250') : true) && + (isValidNonSizedFormat(bid.params.format) || bid.sizes.map(flattenSize).some(isValidSize)); + +/** + * Flattens a 2-element [W, H] array as a 'WxH' string, + * otherwise passes value through. + * @param {Array|String} size + * @returns {String} + */ +const flattenSize = size => + (Array.isArray(size) && size.length === 2) ? `${size[0]}x${size[1]}` : size; + +/** + * Expands a 'WxH' string as a 2-element [W, H] array + * @param {String} size + * @returns {Array} + */ +const expandSize = size => size.split('x').map(Number); + +/** + * Is this a valid slot size? + * @param {String} size + * @returns {Boolean} + */ +const isValidSize = size => includes(['300x250', '320x50'], size); + +/** + * Is this a valid, non-sized format? + * @param {String} size + * @returns {Boolean} + */ +const isValidNonSizedFormat = format => includes(['video', 'native'], format); + +/** + * Is this a valid size and format? + * @param {String} size + * @returns {Boolean} + */ +const isValidSizeAndFormat = (size, format) => + (isFullWidth(format) && flattenSize(size) === '300x250') || + isValidNonSizedFormat(format) || + isValidSize(flattenSize(size)); + +/** + * Find a preferred entry, if any, from an array of valid sizes. + * @param {Array} acc + * @param {String} cur + */ +const sortByPreferredSize = (acc, cur) => + (cur === '300x250') ? [cur, ...acc] : [...acc, cur]; + +/** + * Map any deprecated size/formats to new values. + * @param {String} size + * @param {String} format + */ +const mapDeprecatedSizeAndFormat = (size, format) => + isFullWidth(format) ? ['300x250', null] : [size, format]; + +/** + * Is this a video format? + * @param {String} format + * @returns {Boolean} + */ +const isVideo = format => format === 'video'; + +/** + * Is this a fullwidth format? + * @param {String} format + * @returns {Boolean} + */ +const isFullWidth = format => format === 'fullwidth'; + +/** + * Which SDK version should be used for this format? + * @param {String} format + * @returns {String} + */ +const sdkVersion = format => isVideo(format) ? '' : '6.0.web'; + +/** + * Which platform identifier should be used? + * @param {Array} platforms Possible platform identifiers + * @returns {String} First valid platform found, or default if none found + */ +const findPlatform = platforms => [...platforms.filter(Boolean), platform][0]; + +/** + * Does the search part of the URL contain "anhb_testmode" + * and therefore indicate testmode should be used? + * @returns {String} "true" or "false" + */ +const isTestmode = () => Boolean( + window && window.location && + typeof window.location.search === 'string' && + window.location.search.indexOf('anhb_testmode') !== -1 +).toString(); + +/** + * Generate ad HTML for injection into an iframe + * @param {String} placementId + * @param {String} format + * @param {String} bidId + * @returns {String} HTML + */ +const createAdHtml = (placementId, format, bidId) => { + const nativeStyle = format === 'native' ? '' : ''; + const nativeContainer = format === 'native' ? '
' : ''; + return ` + ${nativeStyle} + +
+ + + ${nativeContainer} +
+ +`; +}; + +/** + * Convert each bid request to a single URL to fetch those bids. + * @param {Array} bids - list of bids + * @param {String} bids[].placementCode - Prebid placement identifier + * @param {Object} bids[].params + * @param {String} bids[].params.placementId - Audience Network placement identifier + * @param {String} bids[].params.platform - Audience Network platform identifier (optional) + * @param {String} bids[].params.format - Optional format, one of 'video' or 'native' if set + * @param {Array} bids[].sizes - list of desired advert sizes + * @param {Array} bids[].sizes[] - Size arrays [h,w]: should include one of [300, 250], [320, 50] + * @returns {Array} List of URLs to fetch, plus formats and sizes for later use with interpretResponse + */ +const buildRequests = (bids, bidderRequest) => { + // Build lists of placementids, adformats, sizes and SDK versions + const placementids = []; + const adformats = []; + const sizes = []; + const sdk = []; + const platforms = []; + const requestIds = []; + + bids.forEach(bid => bid.sizes + .map(flattenSize) + .filter(size => isValidSizeAndFormat(size, bid.params.format)) + .reduce(sortByPreferredSize, []) + .slice(0, 1) + .forEach(preferredSize => { + const [size, format] = mapDeprecatedSizeAndFormat(preferredSize, bid.params.format); + placementids.push(bid.params.placementId); + adformats.push(format || size); + sizes.push(size); + sdk.push(sdkVersion(format)); + platforms.push(bid.params.platform); + requestIds.push(bid.bidId); + }) + ); + // Build URL + const testmode = isTestmode(); + const pageurl = encodeURIComponent(deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || deepAccess(bidderRequest, 'refererInfo.referer')); + const platform = findPlatform(platforms); + const cb = generateUUID(); + const search = { + placementids, + adformats, + testmode, + pageurl, + sdk, + adapterver, + platform, + platver, + cb + }; + const video = findIndex(adformats, isVideo); + if (video !== -1) { + [search.playerwidth, search.playerheight] = expandSize(sizes[video]); + } + const data = formatQS(search); + + return [{ adformats, data, method, requestIds, sizes, url, pageurl }]; +}; + +/** + * Convert a server response to a bid response. + * @param {Object} response - object representing the response + * @param {Object} response.body - response body, already converted from JSON + * @param {Object} bidRequests - the original bid requests + * @param {Array} bidRequest.adformats - list of formats for the original bid requests + * @param {Array} bidRequest.sizes - list of sizes fot the original bid requests + * @returns {Array} A list of bid response objects + */ +const interpretResponse = ({ body }, { adformats, requestIds, sizes, pageurl }) => { + const { bids = {} } = body; + return Object.keys(bids) + // extract Array of bid responses + .map(placementId => bids[placementId]) + // flatten + .reduce((a, b) => a.concat(b), []) + // transform to bidResponse + .map((bid, i) => { + const { + bid_id: fbBidid, + placement_id: creativeId, + bid_price_cents: cpm + } = bid; + + const format = adformats[i]; + const [width, height] = expandSize(flattenSize(sizes[i])); + const ad = createAdHtml(creativeId, format, fbBidid); + const requestId = requestIds[i]; + + const bidResponse = { + // Prebid attributes + requestId, + cpm: cpm / 100, + width, + height, + ad, + ttl, + creativeId, + netRevenue, + currency, + // Audience Network attributes + hb_bidder: hbBidder, + fb_bidid: fbBidid, + fb_format: format, + fb_placementid: creativeId + }; + // Video attributes + if (isVideo(format)) { + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = `https://an.facebook.com/v1/instream/vast.xml?placementid=${creativeId}&pageurl=${pageurl}&playerwidth=${width}&playerheight=${height}&bidid=${fbBidid}`; + bidResponse.ttl = videoTtl; + } + return bidResponse; + }); +}; + +/** + * Covert bid param types for S2S + * @param {Object} params bid params + * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol + * @return {Object} params bid params + */ +const transformBidParams = (params, isOpenRtb) => { + return convertTypes({ + 'placementId': 'string' + }, params); +} + +export const spec = { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse, + transformBidParams +}; + +registerBidder(spec); diff --git a/test/spec/modules/audienceNetworkBidAdapter_spec.js b/test/spec/modules/audienceNetworkBidAdapter_spec.js new file mode 100644 index 00000000000..d92db9f4b73 --- /dev/null +++ b/test/spec/modules/audienceNetworkBidAdapter_spec.js @@ -0,0 +1,568 @@ +/** + * @file Tests for AudienceNetwork adapter. + */ +import { expect } from 'chai'; + +import { spec } from 'modules/audienceNetworkBidAdapter'; +import * as utils from 'src/utils'; + +const { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse +} = spec; + +const bidder = 'audienceNetwork'; +const placementId = 'test-placement-id'; +const playerwidth = 320; +const playerheight = 180; +const requestId = 'test-request-id'; +const debug = 'adapterver=1.3.0&platform=241394079772386&platver=$prebid.version$&cb=test-uuid'; +const expectedPageUrl = encodeURIComponent('http://www.prebid-test.com/audienceNetworkTest.html?pbjs_debug=true'); +const mockBidderRequest = { + bidderCode: 'audienceNetwork', + auctionId: '720146a0-8f32-4db0-bb30-21f1d057efb4', + bidderRequestId: '1312d48561657e', + auctionStart: 1579711852920, + timeout: 3000, + refererInfo: { + referer: 'http://www.prebid-test.com/audienceNetworkTest.html?pbjs_debug=true', + reachedTop: true, + numIframes: 0, + stack: ['http://www.prebid-test.com/audienceNetworkTest.html?pbjs_debug=true'], + canonicalUrl: undefined + }, + start: 1579711852925 +}; + +describe('AudienceNetwork adapter', function () { + describe('Public API', function () { + it('code', function () { + expect(code).to.equal(bidder); + }); + it('supportedMediaTypes', function () { + expect(supportedMediaTypes).to.deep.equal(['banner', 'video']); + }); + it('isBidRequestValid', function () { + expect(isBidRequestValid).to.be.a('function'); + }); + it('buildRequests', function () { + expect(buildRequests).to.be.a('function'); + }); + it('interpretResponse', function () { + expect(interpretResponse).to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('missing placementId parameter', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]] + })).to.equal(false); + }); + + it('invalid sizes parameter', function () { + expect(isBidRequestValid({ + bidder, + sizes: ['', undefined, null, '300x100', [300, 100], [300], {}], + params: { placementId } + })).to.equal(false); + }); + + it('valid when at least one valid size', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[1, 1], [300, 250]], + params: { placementId } + })).to.equal(true); + }); + + it('valid parameters', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250], [320, 50]], + params: { placementId } + })).to.equal(true); + }); + + it('fullwidth', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250], [336, 280]], + params: { + placementId, + format: 'fullwidth' + } + })).to.equal(true); + }); + + it('native', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]], + params: { + placementId, + format: 'native' + } + })).to.equal(true); + }); + + it('native with non-IAB size', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[728, 90]], + params: { + placementId, + format: 'native' + } + })).to.equal(true); + }); + + it('video', function () { + expect(isBidRequestValid({ + bidder, + sizes: [[playerwidth, playerheight]], + params: { + placementId, + format: 'video' + } + })).to.equal(true); + }); + }); + + describe('buildRequests', function () { + before(function () { + sinon + .stub(utils, 'generateUUID') + .returns('test-uuid'); + }); + + after(function () { + utils.generateUUID.restore(); + }); + + it('takes the canonicalUrl as priority over referer for pageurl', function () { + let expectedCanonicalUrl = encodeURIComponent('http://www.this-is-canonical-url.com/hello-world.html?pbjs_debug=true'); + mockBidderRequest.refererInfo.canonicalUrl = 'http://www.this-is-canonical-url.com/hello-world.html?pbjs_debug=true'; + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[300, 50], [300, 250], [320, 50]], + params: { placementId } + }], mockBidderRequest)).to.deep.equal([{ + adformats: ['300x250'], + method: 'GET', + pageurl: expectedCanonicalUrl, + requestIds: [requestId], + sizes: ['300x250'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: `placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=${expectedCanonicalUrl}&sdk[]=6.0.web&${debug}` + }]); + mockBidderRequest.refererInfo.canonicalUrl = undefined; + }); + + it('can build URL for IAB unit', function () { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[300, 50], [300, 250], [320, 50]], + params: { placementId } + }], mockBidderRequest)).to.deep.equal([{ + adformats: ['300x250'], + method: 'GET', + pageurl: expectedPageUrl, + requestIds: [requestId], + sizes: ['300x250'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: `placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=${expectedPageUrl}&sdk[]=6.0.web&${debug}` + }]); + }); + + it('can build URL for video unit', function () { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[640, 480]], + params: { + placementId, + format: 'video' + } + }], mockBidderRequest)).to.deep.equal([{ + adformats: ['video'], + method: 'GET', + pageurl: expectedPageUrl, + requestIds: [requestId], + sizes: ['640x480'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: `placementids[]=test-placement-id&adformats[]=video&testmode=false&pageurl=${expectedPageUrl}&sdk[]=&${debug}&playerwidth=640&playerheight=480` + }]); + }); + + it('can build URL for native unit in non-IAB size', function () { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[728, 90]], + params: { + placementId, + format: 'native' + } + }], mockBidderRequest)).to.deep.equal([{ + adformats: ['native'], + method: 'GET', + pageurl: expectedPageUrl, + requestIds: [requestId], + sizes: ['728x90'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: `placementids[]=test-placement-id&adformats[]=native&testmode=false&pageurl=${expectedPageUrl}&sdk[]=6.0.web&${debug}` + }]); + }); + + it('can build URL for deprecated fullwidth unit, overriding platform', function () { + const platform = 'test-platform'; + const debugPlatform = debug.replace('241394079772386', platform); + + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[300, 250]], + params: { + placementId, + platform, + format: 'fullwidth' + } + }], mockBidderRequest)).to.deep.equal([{ + adformats: ['300x250'], + method: 'GET', + pageurl: expectedPageUrl, + requestIds: [requestId], + sizes: ['300x250'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: `placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=${expectedPageUrl}&sdk[]=6.0.web&${debugPlatform}` + }]); + }); + }); + + describe('interpretResponse', function () { + it('error in response', function () { + expect(interpretResponse({ + body: { + errors: ['test-error-message'] + } + }, {})).to.deep.equal([]); + }); + + it('valid native bid in response', function () { + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['native'], + requestIds: [requestId], + sizes: [[300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.ad) + .to.contain(`placementid: '${placementId}',`) + .and.to.contain(`format: 'native',`) + .and.to.contain(`bidid: 'test-bid-id',`) + .and.to.contain('getElementsByTagName("style")', 'ad missing native styles') + .and.to.contain('
', 'ad missing native container'); + expect(bidResponse.ttl).to.equal(600); + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + + expect(bidResponse.hb_bidder).to.equal('fan'); + expect(bidResponse.fb_bidid).to.equal('test-bid-id'); + expect(bidResponse.fb_format).to.equal('native'); + expect(bidResponse.fb_placementid).to.equal(placementId); + }); + + it('valid IAB bid in response', function () { + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.ad) + .to.contain(`placementid: '${placementId}',`) + .and.to.contain(`format: '300x250',`) + .and.to.contain(`bidid: 'test-bid-id',`) + .and.not.to.contain('getElementsByTagName("style")', 'ad should not contain native styles') + .and.not.to.contain('
', 'ad should not contain native container'); + expect(bidResponse.ttl).to.equal(600); + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.hb_bidder).to.equal('fan'); + expect(bidResponse.fb_bidid).to.equal('test-bid-id'); + expect(bidResponse.fb_format).to.equal('300x250'); + expect(bidResponse.fb_placementid).to.equal(placementId); + }); + + it('filters invalid slot sizes', function () { + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.ttl).to.equal(600); + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.hb_bidder).to.equal('fan'); + expect(bidResponse.fb_bidid).to.equal('test-bid-id'); + expect(bidResponse.fb_format).to.equal('300x250'); + expect(bidResponse.fb_placementid).to.equal(placementId); + }); + + it('valid multiple bids in response', function () { + const placementIdNative = 'test-placement-id-native'; + const placementIdIab = 'test-placement-id-iab'; + + const [bidResponseNative, bidResponseIab] = interpretResponse({ + body: { + errors: [], + bids: { + [placementIdNative]: [{ + placement_id: placementIdNative, + bid_id: 'test-bid-id-native', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }], + [placementIdIab]: [{ + placement_id: placementIdIab, + bid_id: 'test-bid-id-iab', + bid_price_cents: 456, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['native', '300x250'], + requestIds: [requestId, requestId], + sizes: ['300x250', [300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponseNative.cpm).to.equal(1.23); + expect(bidResponseNative.requestId).to.equal(requestId); + expect(bidResponseNative.width).to.equal(300); + expect(bidResponseNative.height).to.equal(250); + expect(bidResponseNative.ad) + .to.contain(`placementid: '${placementIdNative}',`) + .and.to.contain(`format: 'native',`) + .and.to.contain(`bidid: 'test-bid-id-native',`); + expect(bidResponseNative.ttl).to.equal(600); + expect(bidResponseNative.creativeId).to.equal(placementIdNative); + expect(bidResponseNative.netRevenue).to.equal(true); + expect(bidResponseNative.currency).to.equal('USD'); + expect(bidResponseNative.hb_bidder).to.equal('fan'); + expect(bidResponseNative.fb_bidid).to.equal('test-bid-id-native'); + expect(bidResponseNative.fb_format).to.equal('native'); + expect(bidResponseNative.fb_placementid).to.equal(placementIdNative); + + expect(bidResponseIab.cpm).to.equal(4.56); + expect(bidResponseIab.requestId).to.equal(requestId); + expect(bidResponseIab.width).to.equal(300); + expect(bidResponseIab.height).to.equal(250); + expect(bidResponseIab.ad) + .to.contain(`placementid: '${placementIdIab}',`) + .and.to.contain(`format: '300x250',`) + .and.to.contain(`bidid: 'test-bid-id-iab',`); + expect(bidResponseIab.ttl).to.equal(600); + expect(bidResponseIab.creativeId).to.equal(placementIdIab); + expect(bidResponseIab.netRevenue).to.equal(true); + expect(bidResponseIab.currency).to.equal('USD'); + expect(bidResponseIab.hb_bidder).to.equal('fan'); + expect(bidResponseIab.fb_bidid).to.equal('test-bid-id-iab'); + expect(bidResponseIab.fb_format).to.equal('300x250'); + expect(bidResponseIab.fb_placementid).to.equal(placementIdIab); + }); + + it('valid video bid in response', function () { + const bidId = 'test-bid-id-video'; + + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: bidId, + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['video'], + requestIds: [requestId], + sizes: [[playerwidth, playerheight]], + pageurl: expectedPageUrl + }); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.ttl).to.equal(3600); + expect(bidResponse.mediaType).to.equal('video'); + expect(bidResponse.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${placementId}&pageurl=${expectedPageUrl}&playerwidth=${playerwidth}&playerheight=${playerheight}&bidid=${bidId}`); + expect(bidResponse.width).to.equal(playerwidth); + expect(bidResponse.height).to.equal(playerheight); + }); + + it('mixed video and native bids', function () { + const videoPlacementId = 'test-video-placement-id'; + const videoBidId = 'test-video-bid-id'; + const nativePlacementId = 'test-native-placement-id'; + const nativeBidId = 'test-native-bid-id'; + + const [bidResponseVideo, bidResponseNative] = interpretResponse({ + body: { + errors: [], + bids: { + [videoPlacementId]: [{ + placement_id: videoPlacementId, + bid_id: videoBidId, + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }], + [nativePlacementId]: [{ + placement_id: nativePlacementId, + bid_id: nativeBidId, + bid_price_cents: 456, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['video', 'native'], + requestIds: [requestId, requestId], + sizes: [[playerwidth, playerheight], [300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponseVideo.cpm).to.equal(1.23); + expect(bidResponseVideo.requestId).to.equal(requestId); + expect(bidResponseVideo.ttl).to.equal(3600); + expect(bidResponseVideo.mediaType).to.equal('video'); + expect(bidResponseVideo.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${videoPlacementId}&pageurl=${expectedPageUrl}&playerwidth=${playerwidth}&playerheight=${playerheight}&bidid=${videoBidId}`); + expect(bidResponseVideo.width).to.equal(playerwidth); + expect(bidResponseVideo.height).to.equal(playerheight); + + expect(bidResponseNative.cpm).to.equal(4.56); + expect(bidResponseNative.requestId).to.equal(requestId); + expect(bidResponseNative.ttl).to.equal(600); + expect(bidResponseNative.width).to.equal(300); + expect(bidResponseNative.height).to.equal(250); + expect(bidResponseNative.ad) + .to.contain(`placementid: '${nativePlacementId}',`) + .and.to.contain(`format: 'native',`) + .and.to.contain(`bidid: '${nativeBidId}',`); + }); + + it('mixture of valid native bid and error in response', function () { + const [bidResponse] = interpretResponse({ + body: { + errors: ['test-error-message'], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + } + }, { + adformats: ['native'], + requestIds: [requestId], + sizes: [[300, 250]], + pageurl: expectedPageUrl + }); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.ad) + .to.contain(`placementid: '${placementId}',`) + .and.to.contain(`format: 'native',`) + .and.to.contain(`bidid: 'test-bid-id',`) + .and.to.contain('getElementsByTagName("style")', 'ad missing native styles') + .and.to.contain('