diff --git a/CHANGELOG b/CHANGELOG index d03338efd6e..4f332925863 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,14 @@ +AOL Prebid 1.17.0 +---------------- +Added functionality for rendering pixels once. + + +AOL Prebid 1.16.0 +---------------- +Added Audience Network adapter. +Added creative key field in bid response for Sharethrough adapter. + + AOL Prebid 1.15.0 ---------------- Nexage API implemented. diff --git a/adapters.json b/adapters.json index 41948e5920d..c182fe7abfc 100644 --- a/adapters.json +++ b/adapters.json @@ -10,6 +10,7 @@ "aol", "appnexus", "appnexusAst", + "audienceNetwork", "conversant", "districtmDMX", "fidelity", diff --git a/integrationExamples/gpt/audienceNetwork_dfp.html b/integrationExamples/gpt/audienceNetwork_dfp.html new file mode 100644 index 00000000000..b30df31b276 --- /dev/null +++ b/integrationExamples/gpt/audienceNetwork_dfp.html @@ -0,0 +1,83 @@ + + + + + + +

Prebid.js Test

+
+ +
+
+

Audience Network quick start

+
    +
  1. Create a new App at https://developers.facebook.com/apps
  2. +
  3. Add the Audience Network product to it
  4. +
  5. Create a new Placement to generate your placementId
  6. +
  7. To test, ensure the User-Agent request header represents a mobile device
  8. +
+
+ + diff --git a/src/adapters/analytics/aolPartnersIds.json b/src/adapters/analytics/aolPartnersIds.json index cd2e2115c80..ae1fc410c79 100644 --- a/src/adapters/analytics/aolPartnersIds.json +++ b/src/adapters/analytics/aolPartnersIds.json @@ -55,5 +55,6 @@ "twenga": 54, "lifestreet": 55, "vertamedia": 56, - "stickyadstv": 57 + "stickyadstv": 57, + "audienceNetwork": 58 } diff --git a/src/adapters/aol.js b/src/adapters/aol.js index 687bfab4efc..61c45d2ca10 100644 --- a/src/adapters/aol.js +++ b/src/adapters/aol.js @@ -5,6 +5,10 @@ const bidmanager = require('../bidmanager.js'); const constants = require('../constants.json'); const events = require('src/events'); +$$PREBID_GLOBAL$$.aolGlobals = { + pixelsDropped: false +}; + const AolAdapter = function AolAdapter() { let showCpmAdjustmentWarning = true; @@ -67,8 +71,11 @@ const AolAdapter = function AolAdapter() { })(); function dropSyncCookies(pixels) { - let pixelElements = parsePixelItems(pixels); - renderPixelElements(pixelElements); + if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped) { + let pixelElements = parsePixelItems(pixels); + renderPixelElements(pixelElements); + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; + } } function parsePixelItems(pixels) { @@ -227,7 +234,11 @@ const AolAdapter = function AolAdapter() { if (bid.params.userSyncOn === constants.EVENTS.BID_RESPONSE) { dropSyncCookies(response.ext.pixels); } else { - ad += response.ext.pixels; + let formattedPixels = response.ext.pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, ''); + + ad += ''; } } diff --git a/src/adapters/audienceNetwork.js b/src/adapters/audienceNetwork.js new file mode 100644 index 00000000000..8ab96430491 --- /dev/null +++ b/src/adapters/audienceNetwork.js @@ -0,0 +1,208 @@ +/** + * @file AudienceNetwork adapter. + */ +import { ajax } from '../ajax'; +import { createBid } from '../bidfactory'; +import { addBidResponse } from '../bidmanager'; +import { STATUS } from '../constants.json'; +import { format } from '../url'; +import { logError } from '../utils'; +import { createNew } from './adapter'; + +const baseAdapter = createNew('audienceNetwork'); +const setBidderCode = baseAdapter.setBidderCode; +const getBidderCode = baseAdapter.getBidderCode; + +/** + * Does this bid request contain valid parameters? + * @param {Object} bid + * @returns {Boolean} + */ +const validateBidRequest = bid => + typeof bid.params === 'object' && + typeof bid.params.placementId === 'string' && + bid.params.placementId.length > 0 && + Array.isArray(bid.sizes) && bid.sizes.length > 0; + +/** + * Does this bid request contain valid sizes? + * @param {Object} bid + * @returns {Boolean} + */ +const validateBidRequestSizes = bid => { + bid.sizes = bid.sizes.map(flattenSize); + return bid.sizes.every( size => + ['native', 'fullwidth', '300x250', '320x50'].includes(size) ); +}; + +/** + * Flattens a 2-element [W, H] array as a 'WxH' string, + * otherwise passes value through. + * @params {Array|String} size + * @returns {String} + */ +const flattenSize = size => + (Array.isArray(size) && size.length === 2) ? `${size[0]}x${size[1]}` : size; + +/** + * 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(); + +/** + * Parse JSON-as-string into an Object, default to empty. + * @param {String} JSON-as-string + * @returns {Object} + */ +const parseJson = jsonAsString => { + let data = {}; + try { + data = JSON.parse(jsonAsString); + } catch (err) {} + return data; +}; + +/** + * Is this a native advert size? + * @param {String} size + * @returns {Boolean} + */ +const isNative = (size) => ['native', 'fullwidth'].includes(size); + +/** + * Generate ad HTML for injection into an iframe + * @param {String} placementId + * @param {String} size + * @param {String} bidId + * @returns {String} HTML + */ +const createAdHtml = (placementId, size, bidId) => { + const nativeStyle = isNative(size) ? '' : ''; + const nativeContainer = isNative(size) ? '
' : ''; + return `${nativeStyle}
+ +${nativeContainer}
`; +}; + +/** + * Creates a "good" Bid object with the given bid ID and CPM. + * @param {String} placementId + * @param {String} bidId + * @param {String} size + * @param {Number} cpmCents + * @returns {Object} Bid + */ +const createSuccessBidResponse = (placementId, size, bidId, cpmCents) => { + const bid = createBid(STATUS.GOOD, { bidId }); + // Prebid attributes + bid.bidderCode = getBidderCode(); + bid.cpm = cpmCents / 100; + bid.ad = createAdHtml(placementId, size, bidId); + if (!isNative(size)) { + [bid.width, bid.height] = size.split('x').map(Number); + } + // Audience Network attributes + bid.hb_bidder = 'fan'; + bid.fb_bidid = bidId; + bid.fb_format = size; + bid.fb_placementid = placementId; + return bid; +}; + +/** + * Creates a "no bid" Bid object. + * @returns {Object} Bid + */ +const createFailureBidResponse = () => { + const bid = createBid(STATUS.NO_BID); + bid.bidderCode = getBidderCode(); + return bid; +}; + +/** + * Fetch bids for given parameters. + * @param {Object} bidRequest + * @param {Array} params.bids - list of bids + * @param {String} params.bids[].placementCode - Prebid placement identifier + * @param {Object} params.bids[].params + * @param {String} params.bids[].params.placementId - Audience Network placement identifier + * @param {Array} params.bids[].sizes - list of accepted advert sizes + * @param {Array|String} params.bids[].sizes[] - one of 'native', '300x250', '300x50', [300, 250], [300, 50] + * @returns {void} + */ +const callBids = bidRequest => { + // Build lists of adUnitCodes, placementids and adformats + const adUnitCodes = []; + const placementids = []; + const adformats = []; + bidRequest.bids + .filter(validateBidRequest) + .filter(validateBidRequestSizes) + .forEach( bid => bid.sizes.forEach( size => { + adUnitCodes.push(bid.placementCode); + placementids.push(bid.params.placementId); + adformats.push(size); + })); + + if (placementids.length) { + // Build URL + const testmode = isTestmode(); + const url = format({ + protocol: 'https', + host: 'an.facebook.com', + pathname: '/v2/placementbid.json', + search: { + sdk: '5.5.web', + testmode, + placementids, + adformats + } + }); + // Request + ajax(url, res => { + // Handle response + const data = parseJson(res); + if (data.errors && data.errors.length) { + const noBid = createFailureBidResponse(); + adUnitCodes.forEach( adUnitCode => addBidResponse(adUnitCode, noBid) ); + data.errors.forEach(logError); + } else { + // For each placementId in bids Object + Object.keys(data.bids) + // extract Array of bid responses + .map( placementId => data.bids[placementId] ) + // flatten + .reduce( (a, b) => a.concat(b), [] ) + // call addBidResponse + .forEach( (bid, i) => + addBidResponse(adUnitCodes[i], createSuccessBidResponse( + bid.placement_id, adformats[i], bid.bid_id, bid.bid_price_cents + )) + ); + } + }, null, { withCredentials: true }); + } else { + // No valid bids + logError('No valid bids requested'); + } +}; + +/** + * @class AudienceNetwork + * @type {Object} + * @property {Function} callBids - fetch bids for given parameters + * @property {Function} setBidderCode - used for bidder aliasing + * @property {Function} getBidderCode - unique 'audienceNetwork' identifier + */ +const AudienceNetwork = () => { + return { callBids, setBidderCode, getBidderCode }; +}; +module.exports = AudienceNetwork; diff --git a/src/adapters/sharethrough.js b/src/adapters/sharethrough.js index a9d26ecf23e..0f981ca9551 100644 --- a/src/adapters/sharethrough.js +++ b/src/adapters/sharethrough.js @@ -1,9 +1,10 @@ var utils = require('../utils.js'); var bidmanager = require('../bidmanager.js'); var bidfactory = require('../bidfactory.js'); +var ajax = require('../ajax.js').ajax; const STR_BIDDER_CODE = "sharethrough"; -const STR_VERSION = "0.1.0"; //Need to check analytics too for version +const STR_VERSION = "1.2.0"; var SharethroughAdapter = function SharethroughAdapter() { @@ -11,62 +12,42 @@ var SharethroughAdapter = function SharethroughAdapter() { str.STR_BTLR_HOST = document.location.protocol + "//btlr.sharethrough.com"; str.STR_BEACON_HOST = document.location.protocol + "//b.sharethrough.com/butler?"; str.placementCodeSet = {}; + str.ajax = ajax; function _callBids(params) { const bids = params.bids; - addEventListener("message", _receiveMessage, false); - // cycle through bids for (let i = 0; i < bids.length; i += 1) { const bidRequest = bids[i]; str.placementCodeSet[bidRequest.placementCode] = bidRequest; const scriptUrl = _buildSharethroughCall(bidRequest); - str.loadIFrame(scriptUrl); + str.ajax(scriptUrl, _createCallback(bidRequest), undefined, {withCredentials: true}); } } + function _createCallback(bidRequest) { + return (bidResponse) => { + _strcallback(bidRequest, bidResponse); + }; + } + function _buildSharethroughCall(bid) { - const testPkey = 'test'; const pkey = utils.getBidIdParameter('pkey', bid.params); let host = str.STR_BTLR_HOST; let url = host + "/header-bid/v1?"; url = utils.tryAppendQueryString(url, 'bidId', bid.bidId); - - if(pkey !== testPkey) { - url = utils.tryAppendQueryString(url, 'placement_key', pkey); - url = utils.tryAppendQueryString(url, 'ijson', '$$PREBID_GLOBAL$$.strcallback'); - url = appendEnvFields(url); - } else { - url = url.substring(0, url.length - 1); - } + url = utils.tryAppendQueryString(url, 'placement_key', pkey); + url = appendEnvFields(url); return url; } - str.loadIFrame = function(url) { - const iframe = document.createElement("iframe"); - iframe.src = url; - iframe.style.cssText = 'display:none;'; - - document.body.appendChild(iframe); - }; - - function _receiveMessage(event) { - if(event.origin === str.STR_BTLR_HOST) { - try { - $$PREBID_GLOBAL$$.strcallback(JSON.parse(event.data).response); - } catch(e) { - console.log(e); - } - } - } - - $$PREBID_GLOBAL$$.strcallback = function(bidResponse) { + function _strcallback(bidObj, bidResponse) { + bidResponse = JSON.parse(bidResponse); const bidId = bidResponse.bidId; - const bidObj = utils.getBidRequest(bidId); try { const bid = bidfactory.createBid(1, bidObj); bid.bidderCode = STR_BIDDER_CODE; @@ -102,7 +83,7 @@ var SharethroughAdapter = function SharethroughAdapter() { } catch (e) { _handleInvalidBid(bidObj); } - }; + } function _handleInvalidBid(bidObj) { const bid = bidfactory.createBid(2, bidObj); @@ -120,6 +101,7 @@ var SharethroughAdapter = function SharethroughAdapter() { return { callBids: _callBids, str : str, + callback: _strcallback }; }; diff --git a/src/prebid.js b/src/prebid.js index 88ef9b2a7cd..bd534909a3d 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -245,11 +245,11 @@ $$PREBID_GLOBAL$$.setTargetingForGPTAsync = function () { //first reset any old targeting targeting.resetPresetTargeting(); - + //now set new targeting keys targeting.setTargeting(targeting.getAllTargeting()); - - //emit event + + //emit event events.emit(SET_TARGETING); }; @@ -261,8 +261,8 @@ $$PREBID_GLOBAL$$.setTargetingForAst = function() { } targeting.setTargetingForAst(); - - //emit event + + //emit event events.emit(SET_TARGETING); }; diff --git a/test/spec/adapters/aol_spec.js b/test/spec/adapters/aol_spec.js index 47b434de7f2..45e6ea68154 100644 --- a/test/spec/adapters/aol_spec.js +++ b/test/spec/adapters/aol_spec.js @@ -3,41 +3,45 @@ import { cloneDeep } from 'lodash'; import * as utils from 'src/utils'; import AolAdapter from 'src/adapters/aol'; import bidmanager from 'src/bidmanager'; -import events from 'src/events'; -import constants from 'src/constants'; - -const DEFAULT_BIDDER_REQUEST = { - bidderCode: 'aol', - requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - bidderRequestId: '7101db09af0db2', - start: new Date().getTime(), - bids: [{ - bidder: 'aol', - bidId: '84ab500420319d', - bidderRequestId: '7101db09af0db2', - requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - placementCode: 'foo', - params: { - placement: 1234567, - network: '9599.1' - } - }] + + +let getDefaultBidResponse = () => { + return { + "id": "245730051428950632", + "cur": "USD", + "seatbid": [{ + "bid": [{ + "id": 1, + "impid": "245730051428950632", + "price": 0.09, + "adm": "", + "crid": "0", + "h": 90, + "w": 728, + "ext": {"sizeid": 225} + }] + }] + }; }; -const DEFAULT_PUBAPI_RESPONSE = { - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "price": 0.09, - "adm": "", - "crid": "0", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} + +let getDefaultBidRequest = () => { + return { + bidderCode: 'aol', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + bidderRequestId: '7101db09af0db2', + start: new Date().getTime(), + bids: [{ + bidder: 'aol', + bidId: '84ab500420319d', + bidderRequestId: '7101db09af0db2', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + placementCode: 'foo', + params: { + placement: 1234567, + network: '9599.1' + } }] - }] + }; }; describe('AolAdapter', () => { @@ -47,7 +51,7 @@ describe('AolAdapter', () => { beforeEach(() => adapter = new AolAdapter()); function createBidderRequest({bids, params} = {}) { - var bidderRequest = cloneDeep(DEFAULT_BIDDER_REQUEST); + var bidderRequest = getDefaultBidRequest(); if (bids && Array.isArray(bids)) { bidderRequest.bids = bids; } @@ -83,7 +87,7 @@ describe('AolAdapter', () => { }); it('should hit the Marketplace api endpoint with the Marketplace config', () => { - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); expect(requests[0].url).to.contain('adserver-us.adtech.advertising.com/pubapi/3.0/'); }); @@ -121,17 +125,17 @@ describe('AolAdapter', () => { }); it('should be the pubapi bid request', () => { - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); expect(requests[0].url).to.contain('cmd=bid;'); }); it('should be the version 2 of pubapi', () => { - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); expect(requests[0].url).to.contain('v=2;'); }); it('should contain cache busting', () => { - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); expect(requests[0].url).to.match(/misc=\d+/); }); @@ -218,7 +222,6 @@ describe('AolAdapter', () => { })); expect(requests[0].url).to.contain('bidfloor=0.8'); }); - }); describe('Nexage api', () => { @@ -355,14 +358,14 @@ describe('AolAdapter', () => { }); it('should be added to bidmanager if returned from pubapi', () => { - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + server.respondWith(JSON.stringify(getDefaultBidResponse())); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; }); it('should be added to bidmanager if returned from nexage GET bid request', () => { - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); + server.respondWith(JSON.stringify(getDefaultBidResponse())); adapter.callBids(createBidderRequest({ params: { dcn: '54321123', @@ -374,7 +377,7 @@ describe('AolAdapter', () => { }); it('should be added to bidmanager if returned from nexage POST bid request', () => { - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); + server.respondWith(JSON.stringify(getDefaultBidResponse())); adapter.callBids(createBidderRequest({ params: { id: 'id-1', @@ -394,25 +397,25 @@ describe('AolAdapter', () => { }); it('should be added to bidmanager with correct bidderCode', () => { - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + server.respondWith(JSON.stringify(getDefaultBidResponse())); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1]).to.have.property('bidderCode', 'aol'); }); it('should have adId matching the bidId from related bid request', () => { - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + server.respondWith(JSON.stringify(getDefaultBidResponse())); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1]) - .to.have.property('adId', DEFAULT_BIDDER_REQUEST.bids[0].bidId); + .to.have.property('adId', '84ab500420319d'); }); it('should be added to bidmanager as invalid in case of empty response', () => { server.respondWith(''); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); @@ -420,238 +423,129 @@ describe('AolAdapter', () => { it('should be added to bidmanager as invalid in case of invalid JSON response', () => { server.respondWith('{foo:{bar:{baz:'); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); }); it('should be added to bidmanager as invalid in case of no bid data', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid = []; + server.respondWith(JSON.stringify(bidResponse)); + + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); }); it('should have adId matching the bidId from bid request in case of no bid data', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid = []; + server.respondWith(JSON.stringify(bidResponse)); + + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1]) - .to.have.property('adId', DEFAULT_BIDDER_REQUEST.bids[0].bidId); + .to.have.property('adId', '84ab500420319d'); }); it('should be added to bidmanager as invalid in case of empty price', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "adm": "", - "crid": "0", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} - }] - }] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid[0].bid[0].price = undefined; + + server.respondWith(JSON.stringify(bidResponse)); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); }); it('should be added to bidmanager with attributes from pubapi response', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} - }] - }] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid[0].bid[0].crid = '12345'; + + server.respondWith(JSON.stringify(bidResponse)); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; - var bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.ad).to.equal(""); - expect(bidResponse.cpm).to.equal(0.09); - expect(bidResponse.width).to.equal(728); - expect(bidResponse.height).to.equal(90); - expect(bidResponse.creativeId).to.equal('12345'); - expect(bidResponse.pubapiId).to.equal('245730051428950632'); + var addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; + expect(addedBidResponse.ad).to.equal(""); + expect(addedBidResponse.cpm).to.equal(0.09); + expect(addedBidResponse.width).to.equal(728); + expect(addedBidResponse.height).to.equal(90); + expect(addedBidResponse.creativeId).to.equal('12345'); + expect(addedBidResponse.pubapiId).to.equal('245730051428950632'); }); it('should be added to bidmanager including pixels from pubapi response', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} - }] - }], - "ext": { - "pixels": "" - } - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.ext = { + pixels: "" + }; + + server.respondWith(JSON.stringify(bidResponse)); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; - var bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.ad).to.equal( + var addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; + expect(addedBidResponse.ad).to.equal( "" + - "" + "" ); }); it('should be added to bidmanager including dealid from pubapi response', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "dealid": "12345", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": { - "sizeid": 225 - } - }] - }] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid[0].bid[0].dealid = '12345'; + + server.respondWith(JSON.stringify(bidResponse)); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; - var bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.dealId).to.equal('12345'); + var addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; + expect(addedBidResponse.dealId).to.equal('12345'); }); it('should be added to bidmanager including encrypted price from pubapi response', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "dealid": "12345", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": { - "sizeid": 225, - "encp": "a9334987" - } - }] - }] - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.seatbid[0].bid[0].ext.encp = 'a9334987'; + server.respondWith(JSON.stringify(bidResponse)); + + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; - var bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.cpm).to.equal('a9334987'); + let addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; + expect(addedBidResponse.cpm).to.equal('a9334987'); }); it('should not render pixels on pubapi response when no parameter is set', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} - }] - }], - "ext": { - "pixels": "" - } - })); - adapter.callBids(DEFAULT_BIDDER_REQUEST); + let bidResponse = getDefaultBidResponse(); + bidResponse.ext = { + pixels: "" + }; + server.respondWith(JSON.stringify(bidResponse)); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; expect(document.body.querySelectorAll('iframe[src="pixels.org"]').length).to.equal(0); }); it('should render pixels from pubapi response when param userSyncOn is set with \'bidResponse\'', () => { - server.respondWith(JSON.stringify({ - "id": "245730051428950632", - "cur": "USD", - "seatbid": [{ - "bid": [{ - "id": 1, - "impid": "245730051428950632", - "price": 0.09, - "adm": "", - "crid": "12345", - "h": 90, - "w": 728, - "ext": {"sizeid": 225} - }] - }], - "ext": { - "pixels": "" - } - })); - adapter.callBids({ - bidderCode: 'aol', - requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - bidderRequestId: '7101db09af0db2', - start: new Date().getTime(), - bids: [{ - bidder: 'aol', - bidId: '84ab500420319d', - bidderRequestId: '7101db09af0db2', - requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - placementCode: 'foo', - params: { - placement: 1234567, - network: '9599.1', - userSyncOn: 'bidResponse' - } - }] - }); + let bidResponse = getDefaultBidResponse(); + bidResponse.ext = { + pixels: "" + }; + + server.respondWith(JSON.stringify(bidResponse)); + let bidRequest = getDefaultBidRequest(); + bidRequest.bids[0].params.userSyncOn = 'bidResponse'; + adapter.callBids(bidRequest); server.respond(); expect(bidmanager.addBidResponse.calledOnce).to.be.true; @@ -666,8 +560,34 @@ describe('AolAdapter', () => { assertPixelsItem('iframe[src="pixels.org"]'); assertPixelsItem('iframe[src="pixels1.org"]'); + expect($$PREBID_GLOBAL$$.aolGlobals.pixelsDropped).to.be.true; }); + it('should not render pixels if it was rendered before', () => { + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; + let bidResponse = getDefaultBidResponse(); + bidResponse.ext = { + pixels: "" + }; + server.respondWith(JSON.stringify(bidResponse)); + + let bidRequest = getDefaultBidRequest(); + bidRequest.bids[0].params.userSyncOn = 'bidResponse'; + adapter.callBids(bidRequest); + server.respond(); + + expect(bidmanager.addBidResponse.calledOnce).to.be.true; + + let assertPixelsItem = (pixelsItemSelector) => { + let pixelsItems = document.body.querySelectorAll(pixelsItemSelector); + + expect(pixelsItems.length).to.equal(0); + }; + + assertPixelsItem('iframe[src="test.com"]'); + assertPixelsItem('iframe[src="test2.com"]'); + }); }); describe('when bidCpmAdjustment is set', () => { @@ -689,13 +609,13 @@ describe('AolAdapter', () => { it('should show warning in the console', function() { sinon.spy(utils, 'logWarn'); - server.respondWith(JSON.stringify(DEFAULT_PUBAPI_RESPONSE)); + server.respondWith(JSON.stringify(getDefaultBidResponse())); $$PREBID_GLOBAL$$.bidderSettings = { aol: { bidCpmAdjustment: function() {} } }; - adapter.callBids(DEFAULT_BIDDER_REQUEST); + adapter.callBids(getDefaultBidRequest()); server.respond(); expect(utils.logWarn.calledOnce).to.be.true; }); diff --git a/test/spec/adapters/audienceNetwork_spec.js b/test/spec/adapters/audienceNetwork_spec.js new file mode 100644 index 00000000000..09553b80305 --- /dev/null +++ b/test/spec/adapters/audienceNetwork_spec.js @@ -0,0 +1,333 @@ +/** + * @file Tests for AudienceNetwork adapter. + */ +import { expect } from 'chai'; + +import bidmanager from 'src/bidmanager'; +import { STATUS } from 'src/constants.json'; +import * as utils from 'src/utils'; + +import AudienceNetwork from 'src/adapters/audienceNetwork'; + +const bidderCode = 'audienceNetwork'; +const placementId = 'test-placement-id'; +const placementCode = '/test/placement/code'; + +/** + * Expect haystack string to contain needle n times. + * @param {String} haystack + * @param {String} needle + * @param {String} [n=1] + * @throws {Error} + */ +const expectToContain = (haystack, needle, n = 1) => + expect(haystack.split(needle)).to.have.lengthOf(n + 1, + `expected ${n} occurrence(s) of '${needle}' in '${haystack}'`); + + +describe('AudienceNetwork adapter', () => { + + describe('Public API', () => { + const adapter = AudienceNetwork(); + it('getBidderCode', () => { + expect(adapter.getBidderCode).to.be.a('function'); + expect(adapter.getBidderCode()).to.equal(bidderCode); + }); + it('setBidderCode', () => { + expect(adapter.setBidderCode).to.be.a('function'); + }); + it('callBids', () => { + expect(adapter.setBidderCode).to.be.a('function'); + }); + }); + + describe('callBids parameter parsing', () => { + + let xhr; + let requests; + let addBidResponse; + let logError; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + xhr.onCreate = request => requests.push(request); + requests = []; + addBidResponse = sinon.stub(bidmanager, 'addBidResponse'); + logError = sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + xhr.restore(); + bidmanager.addBidResponse.restore(); + utils.logError.restore(); + }); + + it('missing placementId parameter', () => { + // Invalid parameters + const params = { + bidderCode, + bids: [{ + bidder: bidderCode, + sizes: ['native'] + }] + }; + // Request bids + AudienceNetwork().callBids(params); + // Verify no attempt to fetch response + expect(requests).to.have.lengthOf(0); + // Verify no attempt to add a response as no placement was provided + expect(addBidResponse.calledOnce).to.equal(false); + // Verify attempt to log error + expect(logError.calledOnce).to.equal(true); + }); + + it('invalid sizes parameter', () => { + // Invalid parameters + const params = { + bidderCode, + bids: [{ + bidder: bidderCode, + params: { placementId }, + sizes: ['', undefined, null, '300x100', [300, 100], [300], {}] + }] + }; + // Request bids + AudienceNetwork().callBids(params); + // Verify no attempt to fetch response + expect(requests).to.have.lengthOf(0); + // Verify attempt to log error + expect(logError.calledOnce).to.equal(true); + }); + + it('valid parameters', () => { + // Valid parameters + const params = { + bidderCode, + bids: [{ + bidder: bidderCode, + params: { placementId }, + sizes: [[320, 50], [300, 250], '300x250', 'fullwidth', '320x50', 'native'] + }] + }; + // Request bids + AudienceNetwork().callBids(params); + // Verify attempt to fetch response + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('GET'); + expectToContain(requests[0].url, 'https://an.facebook.com/v2/placementbid.json?'); + expectToContain(requests[0].url, 'placementids[]=test-placement-id', 6); + expectToContain(requests[0].url, 'adformats[]=320x50', 2); + expectToContain(requests[0].url, 'adformats[]=300x250', 2); + expectToContain(requests[0].url, 'adformats[]=fullwidth'); + expectToContain(requests[0].url, 'adformats[]=native'); + // Verify no attempt to log error + expect(logError.called).to.equal(false); + }); + + }); + + describe('callBids response handling', () => { + + let server; + let addBidResponse; + let logError; + + beforeEach( () => { + server = sinon.fakeServer.create(); + addBidResponse = sinon.stub(bidmanager, 'addBidResponse'); + logError = sinon.stub(utils, 'logError'); + }); + + afterEach( () => { + server.restore(); + bidmanager.addBidResponse.restore(); + utils.logError.restore(); + }); + + it('error in response', () => { + // Error response + const error = 'test-error-message'; + server.respondWith(JSON.stringify({ + errors: [error] + })); + // Request bids + AudienceNetwork().callBids({ + bidderCode, + bids: [{ + bidder: bidderCode, + params: { placementId }, + sizes: ['native'] + }] + }); + server.respond(); + // Verify attempt to call addBidResponse + expect(addBidResponse.calledOnce).to.equal(true); + expect(addBidResponse.args[0]).to.have.lengthOf(2); + expect(addBidResponse.args[0][1].getStatusCode()).to.equal(STATUS.NO_BID); + expect(addBidResponse.args[0][1].bidderCode).to.equal(bidderCode); + // Verify attempt to log error + expect(logError.calledOnce).to.equal(true); + expect(logError.calledWith(error)).to.equal(true); + }); + + it('valid native bid in response', () => { + // Valid response + server.respondWith(JSON.stringify({ + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + })); + // Request bids + AudienceNetwork().callBids({ + bidderCode, + bids: [{ + bidder: bidderCode, + placementCode, + params: { placementId }, + sizes: ['native'] + }] + }); + server.respond(); + // Verify attempt to call addBidResponse + expect(addBidResponse.calledOnce).to.equal(true); + expect(addBidResponse.args[0]).to.have.lengthOf(2); + expect(addBidResponse.args[0][0]).to.equal(placementCode); + // Verify Prebid attributes in bid response + const bidResponse = addBidResponse.args[0][1]; + expect(bidResponse.getStatusCode()).to.equal(STATUS.GOOD); + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.bidderCode).to.equal(bidderCode); + expect(bidResponse.width).to.equal(0); + expect(bidResponse.height).to.equal(0); + expect(bidResponse.ad).to.contain(`placementid:'${placementId}',format:'native',bidid:'test-bid-id'`, 'ad missing parameters'); + expect(bidResponse.ad).to.contain('getElementsByTagName("style")', 'ad missing native styles'); + expect(bidResponse.ad).to.contain('
', 'ad missing native container'); + // Verify Audience Network attributes in bid response + 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); + // Verify no attempt to log error + expect(logError.called).to.equal(false, 'logError called'); + }); + + it('valid IAB bid in response', () => { + // Valid response + server.respondWith(JSON.stringify({ + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } + })); + // Request bids + AudienceNetwork().callBids({ + bidderCode, + bids: [{ + bidder: bidderCode, + placementCode, + params: { placementId }, + sizes: ['300x250'] + }] + }); + server.respond(); + // Verify attempt to call addBidResponse + expect(addBidResponse.calledOnce).to.equal(true); + expect(addBidResponse.args[0]).to.have.lengthOf(2); + expect(addBidResponse.args[0][0]).to.equal(placementCode); + // Verify bidResponse Object + const bidResponse = addBidResponse.args[0][1]; + expect(bidResponse.getStatusCode()).to.equal(STATUS.GOOD); + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.bidderCode).to.equal(bidderCode); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.ad).to.contain(`placementid:'${placementId}',format:'300x250',bidid:'test-bid-id'`, 'ad missing parameters'); + expect(bidResponse.ad).not.to.contain('getElementsByTagName("style")', 'ad should not contain native styles'); + expect(bidResponse.ad).not.to.contain('
', 'ad should not contain native container'); + // Verify no attempt to log error + expect(logError.called).to.equal(false, 'logError called'); + }); + + it('valid multiple bids in response', () => { + const placementIdNative = 'test-placement-id-native'; + const placementIdIab = 'test-placement-id-iab'; + const placementCodeNative = 'test-placement-code-native'; + const placementCodeIab = 'test-placement-code-iab'; + // Valid response + server.respondWith(JSON.stringify({ + 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' + }] + } + })); + // Request bids + AudienceNetwork().callBids({ + bidderCode, + bids: [{ + bidder: bidderCode, + placementCode: placementCodeNative, + params: { placementId: placementIdNative }, + sizes: ['native'] + }, { + bidder: bidderCode, + placementCode: placementCodeIab, + params: { placementId: placementIdIab }, + sizes: ['300x250'] + }] + }); + server.respond(); + // Verify multiple attempts to call addBidResponse + expect(addBidResponse.calledTwice).to.equal(true); + // Verify native + const addBidResponseNativeCall = addBidResponse.args[0]; + expect(addBidResponseNativeCall).to.have.lengthOf(2); + expect(addBidResponseNativeCall[0]).to.equal(placementCodeNative); + expect(addBidResponseNativeCall[1].getStatusCode()).to.equal(STATUS.GOOD); + expect(addBidResponseNativeCall[1].cpm).to.equal(1.23); + expect(addBidResponseNativeCall[1].bidderCode).to.equal(bidderCode); + expect(addBidResponseNativeCall[1].width).to.equal(0); + expect(addBidResponseNativeCall[1].height).to.equal(0); + expect(addBidResponseNativeCall[1].ad).to.contain(`placementid:'${placementIdNative}',format:'native',bidid:'test-bid-id-native'`, 'ad missing parameters'); + // Verify IAB + const addBidResponseIabCall = addBidResponse.args[1]; + expect(addBidResponseIabCall).to.have.lengthOf(2); + expect(addBidResponseIabCall[0]).to.equal(placementCodeIab); + expect(addBidResponseIabCall[1].getStatusCode()).to.equal(STATUS.GOOD); + expect(addBidResponseIabCall[1].cpm).to.equal(4.56); + expect(addBidResponseIabCall[1].bidderCode).to.equal(bidderCode); + expect(addBidResponseIabCall[1].width).to.equal(300); + expect(addBidResponseIabCall[1].height).to.equal(250); + expect(addBidResponseIabCall[1].ad).to.contain(`placementid:'${placementIdIab}',format:'300x250',bidid:'test-bid-id-iab'`, 'ad missing parameters'); + // Verify no attempt to log error + expect(logError.called).to.equal(false, 'logError called'); + }); + + }); + +}); diff --git a/test/spec/adapters/sharethrough_spec.js b/test/spec/adapters/sharethrough_spec.js index 0678690370e..7ede215d9aa 100644 --- a/test/spec/adapters/sharethrough_spec.js +++ b/test/spec/adapters/sharethrough_spec.js @@ -51,29 +51,20 @@ describe('sharethrough adapter', () => { let secondBidUrl; beforeEach(() => { - sandbox.spy(adapter.str, 'loadIFrame'); + sandbox.spy(adapter.str, 'ajax'); }); - it('should call loadIFrame on the adloader for each bid', () => { + it('should call ajax to make a request for each bid', () => { adapter.callBids(bidderRequest); - firstBidUrl = adapter.str.loadIFrame.firstCall.args[0]; - secondBidUrl = adapter.str.loadIFrame.secondCall.args[0]; + firstBidUrl = adapter.str.ajax.firstCall.args[0]; + secondBidUrl = adapter.str.ajax.secondCall.args[0]; - sinon.assert.calledTwice(adapter.str.loadIFrame); + sinon.assert.calledTwice(adapter.str.ajax); - expect(firstBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId1&placement_key=aaaa1111&ijson=pbjs.strcallback&hbVersion=%24prebid.version%24&strVersion=0.1.0&hbSource=prebid&'); - expect(secondBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId2&placement_key=bbbb2222&ijson=pbjs.strcallback&hbVersion=%24prebid.version%24&strVersion=0.1.0&hbSource=prebid&'); - }); - - }); - - describe('strcallback', () => { - - it('should exist and be a function', () => { - let shit = sandbox.stub(pbjs, 'strcallback'); - expect(pbjs.strcallback).to.exist.and.to.be.a('function'); + expect(firstBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId1&placement_key=aaaa1111&hbVersion=%24prebid.version%24&strVersion=1.2.0&hbSource=prebid&'); + expect(secondBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId2&placement_key=bbbb2222&hbVersion=%24prebid.version%24&strVersion=1.2.0&hbSource=prebid&'); }); }); @@ -117,8 +108,8 @@ describe('sharethrough adapter', () => { "stxUserId": "" }; - pbjs.strcallback(bidderReponse1); - pbjs.strcallback(bidderReponse2); + adapter.callback(bidderRequest.bids[0], JSON.stringify(bidderReponse1)); + adapter.callback(bidderRequest.bids[1], JSON.stringify(bidderReponse2)); firstBid = bidManager.addBidResponse.firstCall.args[1]; secondBid = bidManager.addBidResponse.secondCall.args[1];