From 403a991615271612ab26b5b915f9654972234571 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 26 Oct 2017 13:18:29 +0100 Subject: [PATCH 01/22] Upgrade Audience Network adapter for Prebid 1.0 (#1750) --- modules/audienceNetworkBidAdapter.js | 290 +++---- modules/audienceNetworkBidAdapter.md | 49 ++ .../modules/audienceNetworkBidAdapter_spec.js | 808 +++++++----------- 3 files changed, 456 insertions(+), 691 deletions(-) create mode 100644 modules/audienceNetworkBidAdapter.md diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index de2e3aaff6b..5dfc65aaee2 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -1,23 +1,25 @@ /** * @file AudienceNetwork adapter. */ -import { ajax } from 'src/ajax'; -import { createBid } from 'src/bidfactory'; -import { addBidResponse } from 'src/bidmanager'; -import { STATUS } from 'src/constants.json'; -import { format } from 'src/url'; -import { logError } from 'src/utils'; -import Adapter from 'src/adapter'; -import adaptermanager from 'src/adaptermanager'; - -const { setBidderCode, getBidderCode } = new Adapter('audienceNetwork'); +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import { formatQS } from 'src/url'; +import { getTopWindowUrl } from 'src/utils'; + +const code = 'audienceNetwork'; +const currency = 'USD'; +const method = 'GET'; +const url = 'https://an.facebook.com/v2/placementbid.json'; +const supportedMediaTypes = ['video']; +const netRevenue = true; +const hb_bidder = 'fan'; /** * Does this bid request contain valid parameters? * @param {Object} bid * @returns {Boolean} */ -const validateBidRequest = bid => +const isBidRequestValid = bid => typeof bid.params === 'object' && typeof bid.params.placementId === 'string' && bid.params.placementId.length > 0 && @@ -33,13 +35,6 @@ const validateBidRequest = bid => 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 @@ -72,19 +67,6 @@ const isTestmode = () => Boolean( 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; -}; - /** * Generate ad HTML for injection into an iframe * @param {String} placementId @@ -103,155 +85,121 @@ ${nativeContainer}`; }; /** - * Creates a "good" Bid object with the given bid ID and CPM. - * @param {String} placementId - * @param {String} size - * @param {String} format - * @param {String} bidId - * @param {Number} cpmCents - * @param {String} pageurl - * @returns {Object} Bid - */ -const createSuccessBidResponse = (placementId, size, format, bidId, cpmCents, pageurl) => { - const bid = createBid(STATUS.GOOD, { bidId }); - // Prebid attributes - bid.bidderCode = getBidderCode(); - bid.cpm = cpmCents / 100; - bid.ad = createAdHtml(placementId, format, bidId); - [bid.width, bid.height] = expandSize(size); - - // Audience Network attributes - bid.hb_bidder = 'fan'; - bid.fb_bidid = bidId; - bid.fb_format = format; - bid.fb_placementid = placementId; - // Video attributes - if (isVideo(format)) { - const vast = `https://an.facebook.com/v1/instream/vast.xml?placementid=${placementId}&pageurl=${pageurl}&playerwidth=${bid.width}&playerheight=${bid.height}&bidid=${bidId}`; - bid.mediaType = 'video'; - bid.vastUrl = vast; - bid.descriptionUrl = vast; - } - 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 {String} params.bids[].params.format - Optional format, one of 'video', 'native' or 'fullwidth' if set - * @param {Array} params.bids[].sizes - list of desired advert sizes - * @param {Array} params.bids[].sizes[] - Size arrays [h,w]: should include one of [300, 250], [320, 50]: first matched size is used - * @returns {void} + * 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.format - Optional format, one of 'video', 'native' or 'fullwidth' 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]: first matched size is used + * @returns {Array} List of URLs to fetch, plus formats and sizes for later use with interpretResponse */ -const callBids = bidRequest => { - // Build lists of adUnitCodes, placementids, adformats and sizes - const adUnitCodes = []; +const buildRequests = bids => { + // Build lists of placementids, adformats, sizes and SDK versions const placementids = []; const adformats = []; const sizes = []; const sdk = []; - - bidRequest.bids - .filter(validateBidRequest) - .forEach(bid => bid.sizes - .map(flattenSize) - .filter(size => isValidSize(size) || isVideo(bid.params.format)) - .slice(0, 1) - .forEach(size => { - adUnitCodes.push(bid.placementCode); - placementids.push(bid.params.placementId); - adformats.push(bid.params.format || size); - sizes.push(size); - sdk.push(sdkVersion(bid.params.format)); - }) - ); - - if (placementids.length) { - // Build URL - const testmode = isTestmode(); - const pageurl = encodeURIComponent(location.href); - const search = { - placementids, - adformats, - testmode, - pageurl, - sdk - }; - const video = adformats.findIndex(isVideo); - if (video !== -1) { - [search.playerwidth, search.playerheight] = expandSize(sizes[video]); - } - const url = format({ - protocol: 'https', - host: 'an.facebook.com', - pathname: '/v2/placementbid.json', - search - }); - // 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, - sizes[i], - adformats[i], - bid.bid_id, - bid.bid_price_cents, - pageurl - )) - ); - } - }, null, { withCredentials: true }); - } else { - // No valid bids - logError('No valid bids requested'); + const requestIds = []; + + bids.forEach(bid => bid.sizes + .map(flattenSize) + .filter(size => isValidSize(size) || isVideo(bid.params.format)) + .slice(0, 1) + .forEach(size => { + placementids.push(bid.params.placementId); + adformats.push(bid.params.format || size); + sizes.push(size); + sdk.push(sdkVersion(bid.params.format)); + requestIds.push(bid.bidId); + }) + ); + + // Build URL + const testmode = isTestmode(); + const pageurl = getTopWindowUrl(); + const search = { + placementids, + adformats, + testmode, + pageurl, + sdk + }; + const video = adformats.findIndex(isVideo); + if (video !== -1) { + [search.playerwidth, search.playerheight] = sizes[video].split('x').map(Number) } + const data = formatQS(search); + + return [{ adformats, data, method, requestIds, sizes, url }]; }; /** - * @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 + * 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 */ -function AudienceNetwork() { - return Object.assign(this, { - callBids, - setBidderCode, - getBidderCode - }); -} +const interpretResponse = ({ body }, { adformats, requestIds, sizes }) => { + const ttl = Number(config.getConfig().bidderTimeout); + + return body.errors && body.errors.length + ? [] + : Object.keys(body.bids) + // extract Array of bid responses + .map(placementId => body.bids[placementId]) + // flatten + .reduce((a, b) => a.concat(b), []) + // transform to bidResponse + .map((bid, i) => { + const { + bid_id: fb_bidid, + placement_id: creativeId, + bid_price_cents: cpm + } = bid; + + const format = adformats[i]; + const [width, height] = sizes[i]; + const ad = createAdHtml(creativeId, format, fb_bidid); + const requestId = requestIds[i]; + + const bidResponse = { + // Prebid attributes + requestId, + cpm: cpm / 100, + width, + height, + ad, + ttl, + creativeId, + netRevenue, + currency, + // Audience Network attributes + hb_bidder, + fb_bidid, + fb_format: format, + fb_placementid: creativeId + }; + // Video attributes + if (isVideo(format)) { + const pageurl = getTopWindowUrl(); + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = `https://an.facebook.com/v1/instream/vast.xml?placementid=${creativeId}&pageurl=${encodeURIComponent(pageurl)}&playerwidth=${width}&playerheight=${height}&bidid=${fb_bidid}`; + } + return bidResponse; + }); +}; -adaptermanager.registerBidAdapter(new AudienceNetwork(), 'audienceNetwork', { - supportedMediaTypes: ['video'] -}); +export const spec = { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse +}; -module.exports = AudienceNetwork; +registerBidder(spec); diff --git a/modules/audienceNetworkBidAdapter.md b/modules/audienceNetworkBidAdapter.md new file mode 100644 index 00000000000..72013c8610b --- /dev/null +++ b/modules/audienceNetworkBidAdapter.md @@ -0,0 +1,49 @@ +# Overview + +Module Name: Audience Network Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: Lovell Fuller + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :---------------------------------------------- | :--------------------------------- | +| `placementId` | required | The Placement ID from Audience Network | "555555555555555\_555555555555555" | +| `format` | optional | Format, one of "native", "fullwidth" or "video" | "native" | + +# Example ad units + +```javascript +const adUnits = [{ + code: "test-iab", + sizes: [[300, 250]], + bids: [{ + bidder: "audienceNetwork", + params: { + placementId: "555555555555555_555555555555555" + } + }] +}, { + code: "test-native", + sizes: [[300, 250]], + bids: [{ + bidder: "audienceNetwork", + params: { + format: "native", + placementId: "555555555555555_555555555555555" + } + }] +}, { + code: "test-video", + sizes: [[640, 360]], + bids: [{ + bidder: "audienceNetwork", + params: { + format: "video", + placementId: "555555555555555_555555555555555" + } + }] +}]; +``` diff --git a/test/spec/modules/audienceNetworkBidAdapter_spec.js b/test/spec/modules/audienceNetworkBidAdapter_spec.js index 3dcd4833871..604c3bf0e06 100644 --- a/test/spec/modules/audienceNetworkBidAdapter_spec.js +++ b/test/spec/modules/audienceNetworkBidAdapter_spec.js @@ -3,542 +3,346 @@ */ import { expect } from 'chai'; -import bidmanager from 'src/bidmanager'; -import { STATUS } from 'src/constants.json'; -import * as utils from 'src/utils'; +import { spec } from 'modules/audienceNetworkBidAdapter'; -import AudienceNetwork from 'modules/audienceNetworkBidAdapter'; +const { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse +} = spec; -const bidderCode = 'audienceNetwork'; +const bidder = 'audienceNetwork'; const placementId = 'test-placement-id'; -const placementCode = '/test/placement/code'; const playerwidth = 320; const playerheight = 180; - -/** - * 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}'`); +const requestId = 'test-request-id'; describe('AudienceNetwork adapter', () => { describe('Public API', () => { - const adapter = new AudienceNetwork(); - it('getBidderCode', () => { - expect(adapter.getBidderCode).to.be.a('function'); - expect(adapter.getBidderCode()).to.equal(bidderCode); + it('code', () => { + expect(code).to.equal(bidder); }); - it('setBidderCode', () => { - expect(adapter.setBidderCode).to.be.a('function'); + it('supportedMediaTypes', () => { + expect(supportedMediaTypes).to.deep.equal(['video']); }); - it('callBids', () => { - expect(adapter.setBidderCode).to.be.a('function'); + it('isBidRequestValid', () => { + expect(isBidRequestValid).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'); + it('buildRequests', () => { + expect(buildRequests).to.be.a('function'); }); - - afterEach(() => { - xhr.restore(); - bidmanager.addBidResponse.restore(); - utils.logError.restore(); + it('interpretResponse', () => { + expect(interpretResponse).to.be.a('function'); }); + }); + describe('isBidRequestValid', () => { it('missing placementId parameter', () => { - // Invalid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - sizes: ['native'] - }] - }; - // Request bids - new 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); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]] + })).to.equal(false); }); it('invalid sizes parameter', () => { - // Invalid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: ['', undefined, null, '300x100', [300, 100], [300], {}] - }] - }; - // Request bids - new 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); + expect(isBidRequestValid({ + bidder, + sizes: ['', undefined, null, '300x100', [300, 100], [300], {}], + params: { placementId } + })).to.equal(false); }); - it('filter valid sizes', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[1, 1], [300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=300x250') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + it('valid when at least one valid size', () => { + expect(isBidRequestValid({ + bidder, + sizes: [[1, 1], [300, 250]], + params: { placementId } + })).to.equal(true); }); it('valid parameters', () => { - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[300, 250], [320, 50]] - }, - { - bidder: bidderCode, - params: { placementId }, - sizes: [[320, 50], [300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id&placementids[]=test-placement-id') - .and.to.contain('adformats[]=320x50') - .and.to.contain('adformats[]=300x250') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250], [320, 50]], + params: { placementId } + })).to.equal(true); }); it('fullwidth', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'fullwidth' - }, - sizes: [[300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=fullwidth') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]], + params: { + placementId, + format: 'fullwidth' + } + })).to.equal(true); }); it('native', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'native' - }, - sizes: [[300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=native') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]], + params: { + placementId, + format: 'native' + } + })).to.equal(true); }); it('video', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=video') - .and.to.contain('sdk[]=') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[playerwidth, playerheight]], + params: { + placementId, + format: 'video' + } + })).to.equal(true); }); }); - 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'); + describe('buildRequests', () => { + it('can build URL for IAB unit', () => { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[300, 250], [320, 50]], + params: { placementId } + }])).to.deep.equal([{ + adformats: ['300x250'], + method: 'GET', + requestIds: [requestId], + sizes: ['300x250'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: 'placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=&sdk[]=5.5.web' + }]); }); - afterEach(() => { - server.restore(); - bidmanager.addBidResponse.restore(); - utils.logError.restore(); + it('can build URL for video unit', () => { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[640, 480]], + params: { + placementId, + format: 'video' + } + }])).to.deep.equal([{ + adformats: ['video'], + method: 'GET', + requestIds: [requestId], + sizes: ['640x480'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: 'placementids[]=test-placement-id&adformats[]=video&testmode=false&pageurl=&sdk[]=&playerwidth=640&playerheight=480' + }]); }); + }); + describe('interpretResponse', () => { it('error in response', () => { - // Error response - const error = 'test-error-message'; - server.respondWith(JSON.stringify({ - errors: [error] - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[300, 250]] - }] - }); - 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); + expect(interpretResponse({ + body: { + errors: ['test-error-message'] + } + }, {})).to.deep.equal([]); }); 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' - }] + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId, - format: 'native' - }, - sizes: [[300, 250]] - }] + }, { + adformats: ['native'], + requestIds: [requestId], + sizes: [[300, 250]] }); - 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.requestId).to.equal(requestId); expect(bidResponse.width).to.equal(300); expect(bidResponse.height).to.equal(250); expect(bidResponse.ad) .to.contain(`placementid:'${placementId}',format:'native',bidid:'test-bid-id'`, 'ad missing parameters') .and.to.contain('getElementsByTagName("style")', 'ad missing native styles') .and.to.contain('
', 'ad missing native container'); - // Verify Audience Network attributes in bid response + 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); - // 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' - }] + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: [[300, 250]] - }] + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]] }); - 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.requestId).to.equal(requestId); 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') .and.not.to.contain('getElementsByTagName("style")', 'ad should not contain native styles') .and.not.to.contain('
', 'ad should not contain native container'); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + 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', () => { - // 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' - }] + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: ['350x200'] - }, { - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: [[300, 250]] - }] + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]] }); - 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.requestId).to.equal(requestId); expect(bidResponse.width).to.equal(300); expect(bidResponse.height).to.equal(250); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + 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', () => { 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' - }] + + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode: placementCodeNative, - params: { - placementId: placementIdNative, - format: 'native' - }, - sizes: [[300, 250]] - }, { - bidder: bidderCode, - placementCode: placementCodeIab, - params: { placementId: placementIdIab }, - sizes: [[300, 250]] - }] + }, { + adformats: ['native', '300x250'], + requestIds: [requestId, requestId], + sizes: [[300, 250], [300, 250]] }); - 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(300); - expect(addBidResponseNativeCall[1].height).to.equal(250); - 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'); + + 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}',format:'native',bidid:'test-bid-id-native'`, 'ad missing parameters'); + 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}',format:'300x250',bidid:'test-bid-id-iab'`, 'ad missing parameters'); + 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', () => { const bidId = 'test-bid-id-video'; - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementId]: [{ - placement_id: placementId, - bid_id: bidId, - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }] + }, { + adformats: ['video'], + requestIds: [requestId], + sizes: [[playerwidth, playerheight]] }); - server.respond(); - // Verify addBidResponse call - expect(addBidResponse.calledOnce).to.equal(true); - const addBidResponseArgs = addBidResponse.args[0]; - expect(addBidResponseArgs).to.have.lengthOf(2); - expect(addBidResponseArgs[0]).to.equal(placementCode); - expect(addBidResponseArgs[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseArgs[1].cpm).to.equal(1.23); - expect(addBidResponseArgs[1].bidderCode).to.equal(bidderCode); - // Video-specific properties - expect(addBidResponseArgs[1].mediaType).to.equal('video'); - expect(addBidResponseArgs[1].vastUrl) - .to.equal(addBidResponseArgs[1].descriptionUrl) - .and.to.contain('https://an.facebook.com/v1/instream/vast.xml?') - .and.to.contain(`placementid=${placementId}`) - .and.to.contain('pageurl=http%3A%2F%2F') - .and.to.contain(`playerwidth=${playerwidth}`) - .and.to.contain(`playerheight=${playerheight}`) - .and.to.contain(`bidid=${bidId}`); - expect(addBidResponseArgs[1].width).to.equal(playerwidth); - expect(addBidResponseArgs[1].height).to.equal(playerheight); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.mediaType).to.equal('video'); + expect(bidResponse.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${placementId}&pageurl=&playerwidth=${playerwidth}&playerheight=${playerheight}&bidid=${bidId}`); + expect(bidResponse.width).to.equal(playerwidth); + expect(bidResponse.height).to.equal(playerheight); }); it('mixed video and native bids', () => { @@ -546,81 +350,45 @@ describe('AudienceNetwork adapter', () => { const videoBidId = 'test-video-bid-id'; const nativePlacementId = 'test-native-placement-id'; const nativeBidId = 'test-native-bid-id'; - // Valid response - server.respondWith(JSON.stringify({ - 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' - }] + + 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' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId: videoPlacementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }, { - bidder: bidderCode, - placementCode, - params: { - placementId: nativePlacementId, - format: 'native' - }, - sizes: [[300, 250]] - }] + }, { + adformats: ['video', 'native'], + requestIds: [requestId, requestId], + sizes: [[playerwidth, playerheight], [300, 250]] }); - server.respond(); - // Verify multiple attempts to call addBidResponse - expect(addBidResponse.calledTwice).to.equal(true); - // Verify video - const addBidResponseVideoCall = addBidResponse.args[0]; - expect(addBidResponseVideoCall).to.have.lengthOf(2); - expect(addBidResponseVideoCall[0]).to.equal(placementCode); - expect(addBidResponseVideoCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseVideoCall[1].cpm).to.equal(1.23); - expect(addBidResponseVideoCall[1].bidderCode).to.equal(bidderCode); - // Video-specific properties - expect(addBidResponseVideoCall[1].mediaType).to.equal('video'); - expect(addBidResponseVideoCall[1].vastUrl) - .to.equal(addBidResponseVideoCall[1].descriptionUrl) - .and.to.contain('https://an.facebook.com/v1/instream/vast.xml?') - .and.to.contain(`placementid=${videoPlacementId}`) - .and.to.contain('pageurl=http%3A%2F%2F') - .and.to.contain(`playerwidth=${playerwidth}`) - .and.to.contain(`playerheight=${playerheight}`) - .and.to.contain(`bidid=${videoBidId}`); - expect(addBidResponseVideoCall[1].width).to.equal(playerwidth); - expect(addBidResponseVideoCall[1].height).to.equal(playerheight); - // Verify native - const addBidResponseNativeCall = addBidResponse.args[1]; - expect(addBidResponseNativeCall).to.have.lengthOf(2); - expect(addBidResponseNativeCall[0]).to.equal(placementCode); - expect(addBidResponseNativeCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseNativeCall[1].cpm).to.equal(4.56); - expect(addBidResponseNativeCall[1].bidderCode).to.equal(bidderCode); - expect(addBidResponseNativeCall[1].width).to.equal(300); - expect(addBidResponseNativeCall[1].height).to.equal(250); - expect(addBidResponseNativeCall[1].ad).to.contain(`placementid:'${nativePlacementId}',format:'native',bidid:'${nativeBidId}'`); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + + expect(bidResponseVideo.cpm).to.equal(1.23); + expect(bidResponseVideo.requestId).to.equal(requestId); + expect(bidResponseVideo.mediaType).to.equal('video'); + expect(bidResponseVideo.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${videoPlacementId}&pageurl=&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.width).to.equal(300); + expect(bidResponseNative.height).to.equal(250); + expect(bidResponseNative.ad).to.contain(`placementid:'${nativePlacementId}',format:'native',bidid:'${nativeBidId}'`); }); }); }); From 32196ae40ca9a709e13766982fcc6178313bc358 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Thu, 26 Oct 2017 06:16:22 -0700 Subject: [PATCH 02/22] Fix TypeError condition when bid is empty (#1769) --- modules/dfpAdServerVideo.js | 6 ++++-- test/spec/modules/dfpAdServerVideo_spec.js | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index f2008b806ca..4f56355a70c 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -64,9 +64,11 @@ export default function buildDfpVideoUrl(options) { url: location.href, }; + const adserverTargeting = (bid && bid.adserverTargeting) || {}; + const customParams = Object.assign({}, - bid.adserverTargeting, - { hb_uuid: bid.videoCacheKey }, + adserverTargeting, + { hb_uuid: bid && bid.videoCacheKey }, options.params.cust_params); const queryParams = Object.assign({}, diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 81c83baa65c..3156c628abd 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -91,4 +91,13 @@ describe('The DFP video support module', () => { expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('my_targeting', 'foo'); }); + + it('should work with nobid responses', () => { + const url = buildDfpVideoUrl({ + adUnit: adUnit, + params: { 'iu': 'my/adUnit' } + }); + + expect(url).to.be.a('string'); + }); }); From 1b0439f92075b9bfe70bc9599761d4f3b289f288 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Thu, 26 Oct 2017 10:42:04 -0400 Subject: [PATCH 03/22] Validating bid response params (#1738) * validating bid response params * added all required params * replaced id with correct param requestid * keeping only common keys to validate --- modules/appnexusAstBidAdapter.js | 8 +++- src/adapters/bidderFactory.js | 23 +++++++--- .../modules/appnexusAstBidAdapter_spec.js | 10 +++-- test/spec/unit/core/bidderFactory_spec.js | 42 +++++++++++++++++-- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 9a2cea8a229..3c900e15312 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -182,8 +182,11 @@ function newBid(serverBid, rtbBid) { const bid = { requestId: serverBid.uuid, cpm: rtbBid.cpm, - creative_id: rtbBid.creative_id, + creativeId: rtbBid.creative_id, dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300 }; if (rtbBid.rtb.video) { @@ -191,7 +194,8 @@ function newBid(serverBid, rtbBid) { width: rtbBid.rtb.video.player_width, height: rtbBid.rtb.video.player_height, vastUrl: rtbBid.rtb.video.asset_url, - descriptionUrl: rtbBid.rtb.video.asset_url + descriptionUrl: rtbBid.rtb.video.asset_url, + ttl: 3600 }); // This supports Outstream Video if (rtbBid.renderer_url) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 87dfd372b9c..87d92cbfb92 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -116,6 +116,9 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {string} url The URL which makes the sync happen. */ +// common params for all mediaTypes +const COMMON_BID_RESPONSE_KEYS = ['requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency']; + /** * Register a bidder with prebid, using the given spec. * @@ -303,12 +306,17 @@ export function newBidder(spec) { onResponse(); function addBidUsingRequestMap(bid) { - const bidRequest = bidRequestMap[bid.requestId]; - if (bidRequest) { - const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); - addBidWithCode(bidRequest.placementCode, prebidBid); + // In Prebid 1.0 all the validation logic from bidmanager will move here, as of now we are only validating new params so that adapters dont miss adding them. + if (hasValidKeys(bid)) { + const bidRequest = bidRequestMap[bid.requestId]; + if (bidRequest) { + const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); + addBidWithCode(bidRequest.placementCode, prebidBid); + } else { + logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); + } } else { - logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); + logError(`Bidder ${spec.code} is missing required params. Check http://prebid.org/dev-docs/bidder-adapter-1.html for list of params.`); } } @@ -337,6 +345,11 @@ export function newBidder(spec) { return true; } + function hasValidKeys(bid) { + let bidKeys = Object.keys(bid); + return COMMON_BID_RESPONSE_KEYS.every(key => bidKeys.includes(key)); + } + function newEmptyBid() { const bid = bidfactory.createBid(STATUS.NO_BID); bid.code = spec.code; diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index d76cc08810f..83cbcc38a2b 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -296,18 +296,20 @@ describe('AppNexusAdapter', () => { { 'requestId': '3db3773286ee59', 'cpm': 0.5, - 'creative_id': 29681110, + 'creativeId': 29681110, 'dealId': undefined, 'width': 300, 'height': 250, 'ad': '', - 'mediaType': 'banner' + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true } ]; let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 3260b66da28..e621f1fb329 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -5,6 +5,7 @@ import * as ajax from 'src/ajax'; import { expect } from 'chai'; import { STATUS } from 'src/constants'; import { userSync } from 'src/userSync' +import * as utils from 'src/utils'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -113,7 +114,7 @@ describe('bidders created by newBidder', () => { expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); }); - it("should make no server requests if the spec doesn't return any", () => { + it('should make no server requests if the spec doesn\'t return any', () => { const bidder = newBidder(spec); spec.isBidRequestValid.returns(true); @@ -262,6 +263,7 @@ describe('bidders created by newBidder', () => { describe('when the ajax call succeeds', () => { let ajaxStub; let userSyncStub; + let logErrorSpy; beforeEach(() => { ajaxStub = sinon.stub(ajax, 'ajax', function(url, callbacks) { @@ -270,11 +272,13 @@ describe('bidders created by newBidder', () => { callbacks.success('response body', { getResponseHeader: fakeResponse }); }); userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); }); afterEach(() => { ajaxStub.restore(); userSyncStub.restore(); + utils.logError.restore(); }); it('should call spec.interpretResponse() with the response content', () => { @@ -324,16 +328,21 @@ describe('bidders created by newBidder', () => { expect(spec.interpretResponse.calledTwice).to.equal(true); }); - it("should add bids for each placement code into the bidmanager, even if the bidder doesn't bid on all of them", () => { + it('should add bids for each placement code into the bidmanager, even if the bidder doesn\'t bid on all of them', () => { const bidder = newBidder(spec); const bid = { + creativeId: 'creative-id', + bidderCode: 'code', requestId: 'some-id', ad: 'ad-url.com', cpm: 0.5, height: 200, width: 300, - placementCode: 'mock/placement' + placementCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300 }; spec.isBidRequestValid.returns(true); spec.buildRequests.returns({ @@ -352,6 +361,7 @@ describe('bidders created by newBidder', () => { [bidmanager.addBidResponse.firstCall.args[0], bidmanager.addBidResponse.secondCall.args[0]]; expect(placementsWithBids).to.contain('mock/placement'); expect(placementsWithBids).to.contain('mock/placement2'); + expect(logErrorSpy.callCount).to.equal(0); }); it('should call spec.getUserSyncs() with the response', () => { @@ -391,6 +401,32 @@ describe('bidders created by newBidder', () => { expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); + + it('should logError when required bid response params are missing', () => { + const bidder = newBidder(spec); + + const bid = { + requestId: 'some-id', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(logErrorSpy.calledOnce).to.equal(true); + }); }); describe('when the ajax call fails', () => { From 698c3300754ac3abb233f5cbe9e56b6303ddf4a1 Mon Sep 17 00:00:00 2001 From: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Date: Thu, 26 Oct 2017 16:46:44 +0200 Subject: [PATCH 04/22] nanointeractive bid adapter (#1627) * nanointeractive bid adapter * nanointeractive bid adapter * - using utils.getParameterByName instead of utils.getTopWindowLocation().href - bidderCode is removed - Default ALG changed to 'ihr' - added protocol to 'cors' param * markdown file * enabling localhost for bid requests --- modules/nanointeractiveBidAdapter.js | 88 ++++++++++++++ modules/nanointeractiveBidAdapter.md | 69 +++++++++++ .../modules/nanointeractiveBidAdapter_spec.js | 109 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 modules/nanointeractiveBidAdapter.js create mode 100644 modules/nanointeractiveBidAdapter.md create mode 100644 test/spec/modules/nanointeractiveBidAdapter_spec.js diff --git a/modules/nanointeractiveBidAdapter.js b/modules/nanointeractiveBidAdapter.js new file mode 100644 index 00000000000..9781e36edb6 --- /dev/null +++ b/modules/nanointeractiveBidAdapter.js @@ -0,0 +1,88 @@ +import * as utils from 'src/utils'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { BANNER } from '../src/mediaTypes'; + +export const BIDDER_CODE = 'nanointeractive'; +export const ENGINE_BASE_URL = 'http://tmp.audiencemanager.de/hb'; + +export const SECURITY = 'sec'; +export const DATA_PARTNER_ID = 'dpid'; +export const DATA_PARTNER_PIXEL_ID = 'pid'; +export const ALG = 'alg'; +export const NQ = 'nq'; +export const NQ_NAME = 'name'; +export const CATEGORY = 'category'; + +const DEFAULT_ALG = 'ihr'; + +export const spec = { + + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + const sec = bid.params[SECURITY]; + const dpid = bid.params[DATA_PARTNER_ID]; + const pid = bid.params[DATA_PARTNER_PIXEL_ID]; + return !!(sec && dpid && pid); + }, + buildRequests(bidRequests) { + let payload = []; + bidRequests.forEach(bid => payload.push(createSingleBidRequest(bid))); + return { + method: 'POST', + url: ENGINE_BASE_URL, + data: JSON.stringify(payload) + }; + }, + interpretResponse(serverResponse) { + const bids = []; + serverResponse.forEach(serverBid => { + if (isEngineResponseValid(serverBid)) { + bids.push(createSingleBidResponse(serverBid)); + } + }); + return bids; + } +}; + +function createSingleBidRequest(bid) { + return { + [SECURITY]: bid.params[SECURITY], + [DATA_PARTNER_ID]: bid.params[DATA_PARTNER_ID], + [DATA_PARTNER_PIXEL_ID]: bid.params[DATA_PARTNER_PIXEL_ID], + [ALG]: bid.params[ALG] || DEFAULT_ALG, + [NQ]: [createNqParam(bid), createCategoryParam(bid)], + sizes: bid.sizes.map(value => value[0] + 'x' + value[1]), + bidId: bid.bidId, + cors: location.origin + }; +} + +function createSingleBidResponse(serverBid) { + return { + requestId: serverBid.id, + cpm: serverBid.cpm, + width: serverBid.width, + height: serverBid.height, + ad: serverBid.ad, + ttl: serverBid.ttl, + creativeId: serverBid.creativeId, + netRevenue: serverBid.netRevenue || true, + currency: serverBid.currency, + }; +} + +function createNqParam(bid) { + return bid.params[NQ_NAME] ? utils.getParameterByName(bid.params[NQ_NAME]) : bid.params[NQ] || null; +} + +function createCategoryParam(bid) { + return bid.params[CATEGORY] || null; +} + +function isEngineResponseValid(response) { + return !!response.cpm && !!response.ad; +} + +registerBidder(spec); diff --git a/modules/nanointeractiveBidAdapter.md b/modules/nanointeractiveBidAdapter.md new file mode 100644 index 00000000000..0159353750a --- /dev/null +++ b/modules/nanointeractiveBidAdapter.md @@ -0,0 +1,69 @@ +# Overview + +``` +Module Name: NanoInteractive Bid Adapter +Module Type: Bidder Adapter +Maintainer: rade@nanointeractive.com +``` + +# Description + +Connects to NanoInteractive search retargeting Ad Server for bids. + +Besides standard params, please provide, if exist, user search params. + +Three examples calling the Ad Server. + +**First** is basic + +**Second** is with hardcoded nq (user search) params + +**Third** is with the search query param name of the current url + + +# Test Parameters +``` +var adUnits = [ + // Basic call + { + code: 'basic-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163' + } + }] + }, + // Hardcoded user search + { + code: 'nq-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163', + nq: 'user search' + } + }] + }, + // URL user search + { + code: 'url-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163', + name: 'search' + } + }] + } +]; +``` diff --git a/test/spec/modules/nanointeractiveBidAdapter_spec.js b/test/spec/modules/nanointeractiveBidAdapter_spec.js new file mode 100644 index 00000000000..b9b9207aca2 --- /dev/null +++ b/test/spec/modules/nanointeractiveBidAdapter_spec.js @@ -0,0 +1,109 @@ +import { expect } from 'chai'; +import { + ALG, + BIDDER_CODE, CATEGORY, DATA_PARTNER_ID, DATA_PARTNER_PIXEL_ID, ENGINE_BASE_URL, NQ, NQ_NAME, SECURITY, + spec +} from '../../../modules/nanointeractiveBidAdapter'; + +describe('nanointeractive adapter tests', function () { + const SEARCH_QUERY = 'rumpelstiltskin'; + const WIDTH = 300; + const HEIGHT = 250; + const SIZES = [[WIDTH, HEIGHT]]; + const AD = ' '; + const CPM = 1; + + function getBid(isValid) { + return { + bidder: BIDDER_CODE, + params: (function () { + return { + [SECURITY]: isValid === true ? 'sec1' : null, + [DATA_PARTNER_ID]: 'dpid1', + [DATA_PARTNER_PIXEL_ID]: 'pid1', + [ALG]: 'ihr', + [NQ]: SEARCH_QUERY, + [NQ_NAME]: null, + [CATEGORY]: null, + } + })(), + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: 'ee335735-ddd3-41f2-b6c6-e8aa99f81c0f', + sizes: SIZES, + bidId: '24a1c9ec270973', + bidderRequestId: '189135372acd55', + requestId: 'ac15bb68-4ef0-477f-93f4-de91c47f00a9' + } + } + + const SINGlE_BID_REQUEST = { + [SECURITY]: 'sec1', + [DATA_PARTNER_ID]: 'dpid1', + [DATA_PARTNER_PIXEL_ID]: 'pid1', + [ALG]: 'ihr', + [NQ]: [SEARCH_QUERY, null], + sizes: [WIDTH + 'x' + HEIGHT], + bidId: '24a1c9ec270973', + cors: 'http://localhost:9876' + }; + + function getSingleBidResponse(isValid) { + return { + id: '24a1c9ec270973', + cpm: isValid === true ? CPM : null, + width: WIDTH, + height: HEIGHT, + ad: AD, + ttl: 360, + creativeId: 'TEST_ID', + netRevenue: false, + currency: 'EUR', + } + } + + const VALID_BID = { + requestId: '24a1c9ec270973', + cpm: CPM, + width: WIDTH, + height: HEIGHT, + ad: AD, + ttl: 360, + creativeId: 'TEST_ID', + netRevenue: false, + currency: 'EUR', + }; + + describe('NanoAdapter', () => { + let nanoBidAdapter = spec; + + describe('Methods', () => { + it('Test isBidRequestValid() with valid param', function () { + expect(nanoBidAdapter.isBidRequestValid(getBid(true))).to.equal(true); + }); + it('Test isBidRequestValid() with invalid param', function () { + expect(nanoBidAdapter.isBidRequestValid(getBid(false))).to.equal(false); + }); + it('Test buildRequests()', function () { + let request = nanoBidAdapter.buildRequests([getBid(true)]); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENGINE_BASE_URL); + expect(request.data).to.equal(JSON.stringify([SINGlE_BID_REQUEST])); + }); + it('Test interpretResponse() length', function () { + let bids = nanoBidAdapter.interpretResponse([getSingleBidResponse(true), getSingleBidResponse(false)]); + expect(bids.length).to.equal(1); + }); + it('Test interpretResponse() bids', function () { + let bid = nanoBidAdapter.interpretResponse([getSingleBidResponse(true), getSingleBidResponse(false)])[0]; + expect(bid.requestId).to.equal(VALID_BID.requestId); + expect(bid.cpm).to.equal(VALID_BID.cpm); + expect(bid.width).to.equal(VALID_BID.width); + expect(bid.height).to.equal(VALID_BID.height); + expect(bid.ad).to.equal(VALID_BID.ad); + expect(bid.ttl).to.equal(VALID_BID.ttl); + expect(bid.creativeId).to.equal(VALID_BID.creativeId); + expect(bid.currency).to.equal(VALID_BID.currency); + }); + }); + }); +}); From 15b2798924700abbb6b81c71d626ba8db5be96a6 Mon Sep 17 00:00:00 2001 From: dbemiller Date: Thu, 26 Oct 2017 11:52:22 -0400 Subject: [PATCH 05/22] Fixed the argument type on getUserSyncs. (#1767) --- src/adapters/bidderFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 87d92cbfb92..241891640c5 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -47,7 +47,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {function(ServerResponse, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, * interpret it and return the Bid objects. This function will be run inside a try/catch. * If it throws any errors, your bids will be discarded. - * @property {function(SyncOptions, Array): UserSync[]} [getUserSyncs] Given an array of all the responses + * @property {function(SyncOptions, ServerResponse[]): UserSync[]} [getUserSyncs] Given an array of all the responses * from the server, determine which user syncs should occur. The argument array will contain every element * which has been sent through to interpretResponse. The order of syncs in this array matters. The most * important ones should come first, since publishers may limit how many are dropped on their page. From c730f95444aa344a68dfdf2508c75ef83dcb6ca4 Mon Sep 17 00:00:00 2001 From: jbartek-improve <31618107+jbartek-improve@users.noreply.github.com> Date: Thu, 26 Oct 2017 17:53:47 +0200 Subject: [PATCH 06/22] Update Improve Digital adapter for Prebid 1.0 (#1728) * Update Improve Digital adapter for Prebid 1.0 * Removed bidderCode from bids * Added creativeId to bid response; updated format of the first argument of interpretResponse --- modules/improvedigitalBidAdapter.js | 379 ++++----- modules/improvedigitalBidAdapter.md | 47 + .../modules/improvedigitalBidAdapter_spec.js | 804 ++++++------------ 3 files changed, 493 insertions(+), 737 deletions(-) create mode 100644 modules/improvedigitalBidAdapter.md diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 51fb61b03e2..bc00127b269 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -1,176 +1,150 @@ -const LIB_VERSION_GLOBAL = '3.0.5'; - -const CONSTANTS = require('src/constants'); -const utils = require('src/utils'); -const bidfactory = require('src/bidfactory'); -const bidmanager = require('src/bidmanager'); -const adloader = require('src/adloader'); -const Adapter = require('src/adapter').default; -const adaptermanager = require('src/adaptermanager'); - -const IMPROVE_DIGITAL_BIDDER_CODE = 'improvedigital'; - -const ImproveDigitalAdapter = function () { - let baseAdapter = new Adapter(IMPROVE_DIGITAL_BIDDER_CODE); - baseAdapter.idClient = new ImproveDigitalAdServerJSClient('hb'); - - const LIB_VERSION = LIB_VERSION_GLOBAL; +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { userSync } from 'src/userSync'; + +const BIDDER_CODE = 'improvedigital'; + +export const spec = { + version: '4.0.0', + code: BIDDER_CODE, + aliases: ['id'], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests) { + let normalizedBids = bidRequests.map((bidRequest) => { + return getNormalizedBidRequest(bidRequest); + }); - // Ad server needs to implement JSONP using this function as the callback - const CALLBACK_FUNCTION = '$$PREBID_GLOBAL$$' + '.improveDigitalResponse'; + let idClient = new ImproveDigitalAdServerJSClient('hb'); + let requestParameters = { + singleRequestMode: false, + httpRequestType: idClient.CONSTANTS.HTTP_REQUEST_TYPE.GET, + returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.PREBID, + libVersion: this.version + }; - baseAdapter.getNormalizedBidRequest = function(bid) { - let adUnitId = utils.getBidIdParameter('placementCode', bid) || null; - let placementId = utils.getBidIdParameter('placementId', bid.params) || null; - let publisherId = null; - let placementKey = null; + let requestObj = idClient.createRequest( + normalizedBids, // requestObject + requestParameters + ); - if (placementId === null) { - publisherId = utils.getBidIdParameter('publisherId', bid.params) || null; - placementKey = utils.getBidIdParameter('placementKey', bid.params) || null; + if (requestObj.errors && requestObj.errors.length > 0) { + utils.logError('ID WARNING 0x01'); } - let keyValues = utils.getBidIdParameter('keyValues', bid.params) || null; - let localSize = utils.getBidIdParameter('size', bid.params) || null; - let bidId = utils.getBidIdParameter('bidId', bid); - let normalizedBidRequest = {}; - if (placementId) { - normalizedBidRequest.placementId = placementId; - } else { - if (publisherId) { - normalizedBidRequest.publisherId = publisherId; - } - if (placementKey) { - normalizedBidRequest.placementKey = placementKey; + return requestObj.requests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, request) { + const bids = []; + utils._each(serverResponse.body.bid, function (bidObject) { + if (!bidObject.price || bidObject.price === null || + bidObject.hasOwnProperty('errorCode') || + typeof bidObject.adm !== 'string') { + return; } - } - if (keyValues) { - normalizedBidRequest.keyValues = keyValues; - } - if (localSize && localSize.w && localSize.h) { - normalizedBidRequest.size = {}; - normalizedBidRequest.size.h = localSize.h; - normalizedBidRequest.size.w = localSize.w; - } - if (bidId) { - normalizedBidRequest.id = bidId; - } - if (adUnitId) { - normalizedBidRequest.adUnitId = adUnitId; - } - return normalizedBidRequest; - } - - let submitNoBidResponse = function(bidRequest) { - let bid = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bidRequest); - bid.bidderCode = IMPROVE_DIGITAL_BIDDER_CODE; - bidmanager.addBidResponse(bidRequest.placementCode, bid); - }; - - $$PREBID_GLOBAL$$.improveDigitalResponse = function(response) { - let bidRequests = utils.getBidderRequestAllAdUnits(IMPROVE_DIGITAL_BIDDER_CODE); - if (bidRequests && bidRequests.bids && bidRequests.bids.length > 0) { - utils._each(bidRequests.bids, function (bidRequest) { - let bidObjects = response.bid || []; - utils._each(bidObjects, function (bidObject) { - if (bidObject.id === bidRequest.bidId) { - if (!bidObject.price || bidObject.price === null) { - submitNoBidResponse(bidRequest); - return; - } - if (bidObject.errorCode && bidObject.errorCode !== 0) { - submitNoBidResponse(bidRequest); - return; - } - if (!bidObject.adm || bidObject.adm === null || typeof bidObject.adm !== 'string') { - submitNoBidResponse(bidRequest); - return; - } - - let bid = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bidRequest); - - let syncString = ''; - let syncArray = (bidObject.sync && bidObject.sync.length > 0) ? bidObject.sync : []; + let bid = {}; + let nurl = ''; + if (bidObject.nurl && bidObject.nurl.length > 0) { + nurl = ``; + } + bid.ad = `${nurl}`; + bid.adId = bidObject.id; + bid.cpm = parseFloat(bidObject.price); + bid.creativeId = bidObject.crid; + bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; + if (utils.isNumber(bidObject.lid)) { + bid.dealId = bidObject.lid; + } else if (typeof bidObject.lid === 'object' && bidObject.lid['1']) { + bid.dealId = bidObject.lid['1']; + } + bid.height = bidObject.h; + bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; + bid.requestId = bidObject.id; + bid.width = bidObject.w; - utils._each(syncArray, function (syncElement) { - let syncInd = syncElement.replace(/\//g, '\\\/'); - syncString = `${syncString}${(syncString === '') ? 'document.writeln(\"' : ''}`; - }); - syncString = `${syncString}${(syncString === '') ? '' : '\")'}`; + bids.push(bid); - let nurl = ''; - if (bidObject.nurl && bidObject.nurl.length > 0) { - nurl = ``; - } - bid.ad = `${nurl}`; - bid.bidderCode = IMPROVE_DIGITAL_BIDDER_CODE; - bid.cpm = parseFloat(bidObject.price); - bid.width = bidObject.w; - bid.height = bidObject.h; - - bidmanager.addBidResponse(bidRequest.placementCode, bid); - } + // Register user sync URLs + if (utils.isArray(bidObject.sync)) { + utils._each(bidObject.sync, function (syncElement) { + userSync.registerSync('image', spec.code, syncElement); }); - }); - } - }; - - baseAdapter.callBids = function (params) { - // params will contain an array - let bidRequests = params.bids || []; - let loc = utils.getTopWindowLocation(); - let requestParameters = { - singleRequestMode: false, - httpRequestType: this.idClient.CONSTANTS.HTTP_REQUEST_TYPE.GET, - callback: CALLBACK_FUNCTION, - secure: (loc.protocol === 'https:') ? 1 : 0, - libVersion: this.LIB_VERSION - }; - - let normalizedBids = bidRequests.map((bidRequest) => { - let normalizedBidRequest = this.getNormalizedBidRequest(bidRequest); - if (bidRequest.params && bidRequest.params.singleRequest) { - requestParameters.singleRequestMode = true; } - return normalizedBidRequest; }); + return bids; + } +}; - let request = this.idClient.createRequest( - normalizedBids, // requestObject - requestParameters - ); +function getNormalizedBidRequest(bid) { + let adUnitId = utils.getBidIdParameter('adUnitCode', bid) || null; + let placementId = utils.getBidIdParameter('placementId', bid.params) || null; + let publisherId = null; + let placementKey = null; - if (request.errors && request.errors.length > 0) { - utils.logError('ID WARNING 0x01'); - } + if (placementId === null) { + publisherId = utils.getBidIdParameter('publisherId', bid.params) || null; + placementKey = utils.getBidIdParameter('placementKey', bid.params) || null; + } + let keyValues = utils.getBidIdParameter('keyValues', bid.params) || null; + let localSize = utils.getBidIdParameter('size', bid.params) || null; + let bidId = utils.getBidIdParameter('bidId', bid); + let transactionId = utils.getBidIdParameter('transactionId', bid); - if (request && request.requests && request.requests[0]) { - utils._each(request.requests, function (requestElement) { - if (requestElement.url) { - adloader.loadScript(requestElement.url, null); - } - }); + let normalizedBidRequest = {}; + if (placementId) { + normalizedBidRequest.placementId = placementId; + } else { + if (publisherId) { + normalizedBidRequest.publisherId = publisherId; + } + if (placementKey) { + normalizedBidRequest.placementKey = placementKey; } } - // Export the callBids function, so that prebid.js can execute this function - // when the page asks to send out bid requests. - return Object.assign(this, { - LIB_VERSION: LIB_VERSION, - idClient: baseAdapter.idClient, - getNormalizedBidRequest: baseAdapter.getNormalizedBidRequest, - callBids: baseAdapter.callBids - }); -}; - -ImproveDigitalAdapter.createNew = function () { - return new ImproveDigitalAdapter(); -}; - -adaptermanager.registerBidAdapter(new ImproveDigitalAdapter(), IMPROVE_DIGITAL_BIDDER_CODE); - -module.exports = ImproveDigitalAdapter; + if (keyValues) { + normalizedBidRequest.keyValues = keyValues; + } + if (localSize && localSize.w && localSize.h) { + normalizedBidRequest.size = {}; + normalizedBidRequest.size.h = localSize.h; + normalizedBidRequest.size.w = localSize.w; + } + if (bidId) { + normalizedBidRequest.id = bidId; + } + if (adUnitId) { + normalizedBidRequest.adUnitId = adUnitId; + } + if (transactionId) { + normalizedBidRequest.transactionId = transactionId; + } + return normalizedBidRequest; +} +registerBidder(spec); function ImproveDigitalAdServerJSClient(endPoint) { this.CONSTANTS = { @@ -184,13 +158,17 @@ function ImproveDigitalAdServerJSClient(endPoint) { }, AD_SERVER_BASE_URL: 'ad.360yield.com', END_POINT: endPoint || 'hb', - AD_SERVER_URL_PARAM: '?jsonp=', - CLIENT_VERSION: 'JS-4.0.2', + AD_SERVER_URL_PARAM: 'jsonp=', + CLIENT_VERSION: 'JS-4.2.0', MAX_URL_LENGTH: 2083, ERROR_CODES: { BAD_HTTP_REQUEST_TYPE_PARAM: 1, MISSING_PLACEMENT_PARAMS: 2, LIB_VERSION_MISSING: 3 + }, + RETURN_OBJ_TYPE: { + DEFAULT: 0, + PREBID: 1 } }; @@ -210,6 +188,8 @@ function ImproveDigitalAdServerJSClient(endPoint) { return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); } + requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; + let impressionObjects = []; let impressionObject; let counter; @@ -223,12 +203,19 @@ function ImproveDigitalAdServerJSClient(endPoint) { impressionObjects.push(impressionObject); } + let returnIdMappings = true; + if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.PREBID) { + returnIdMappings = false; + } + let returnObject = {}; - returnObject.idMappings = []; returnObject.requests = []; + if (returnIdMappings) { + returnObject.idMappings = []; + } let errors = null; - let baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + let baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; let bidRequestObject = { bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) @@ -243,49 +230,39 @@ function ImproveDigitalAdServerJSClient(endPoint) { adUnitId: impressionObject.adUnitId }); } else { - returnObject.idMappings.push({ - adUnitId: impressionObject.adUnitId, - id: impressionObject.impressionObject.id - }); - bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; - - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - let outputUri = encodeURIComponent(baseUrl + JSON.stringify(bidRequestObject)); - - if (!requestParameters.singleRequestMode) { - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + if (returnIdMappings) { + returnObject.idMappings.push({ + adUnitId: impressionObject.adUnitId, + id: impressionObject.impressionObject.id }); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; } + bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; + bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); + let writeLongRequest = false; + const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)); if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) { + writeLongRequest = true; if (bidRequestObject.bid_request.imp.length > 1) { + // Pop the current request and process it again in the next iteration bidRequestObject.bid_request.imp.pop(); - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) - }); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - bidRequestObject.bid_request.imp = []; - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - } else { - // We have a problem. Single request is too long for a URI + if (returnIdMappings) { + returnObject.idMappings.pop(); + } + counter--; } } + + if (writeLongRequest || + !requestParameters.singleRequestMode || + counter === impressionObjects.length - 1) { + returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); + bidRequestObject = { + bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) + }; + } } } - if (bidRequestObject.bid_request && - bidRequestObject.bid_request.imp && - bidRequestObject.bid_request.imp.length > 0) { - returnObject.requests = returnObject.requests || []; - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) - }); - } if (errors) { returnObject.errors = errors; @@ -294,6 +271,24 @@ function ImproveDigitalAdServerJSClient(endPoint) { return returnObject; }; + this.formatRequest = function(requestParameters, bidRequestObject) { + switch (requestParameters.returnObjType) { + case this.CONSTANTS.RETURN_OBJ_TYPE.PREBID: + return { + method: 'GET', + url: `//${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}`, + data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${JSON.stringify(bidRequestObject)}` + }; + default: + const baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://` + + `${this.CONSTANTS.AD_SERVER_BASE_URL}/` + + `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + return { + url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + } + } + }; + this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) { let impressionBidRequestObject = {}; if (requestParameters.requestId) { diff --git a/modules/improvedigitalBidAdapter.md b/modules/improvedigitalBidAdapter.md new file mode 100644 index 00000000000..3d91d4f82f2 --- /dev/null +++ b/modules/improvedigitalBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +**Module Name**: Improve Digital Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: hb@improvedigital.com + +# Description + +Module that connects to Improve Digital's demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'div-gpt-ad-1499748733608-0', + bids: [ + { + bidder: 'improvedigital', + params: { + placementId:1053688 + } + } + ] + }, { + code: 'div-gpt-ad-1499748833901-0', + bids: [{ + bidder: 'improvedigital', + params: { + placementId:1053689, + keyValues: { + testKey: ["testValue"] + } + } + }] + }, { + code: 'div-gpt-ad-1499748913322-0', + bids: [{ + bidder: 'improvedigital', + params: { + placementId:1053687, + size: { + w:300, + h:300 + } + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 5b0a9d37d57..750eecc2a7d 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,599 +1,313 @@ -describe('improvedigital adapter tests', function () { - const expect = require('chai').expect; - const Adapter = require('modules/improvedigitalBidAdapter'); - const bidmanager = require('src/bidmanager'); - const adloader = require('src/adloader'); - const constants = require('src/constants.json'); - var bidfactory = require('src/bidfactory'); - var utils = require('src/utils.js'); - - var improveDigitalAdapter, - sandbox, - bidsRequestedOriginal; +import { expect } from 'chai'; +import { ImproveDigitalAdServerJSClient, spec } from 'modules/improvedigitalBidAdapter'; +import { userSync } from 'src/userSync'; + +describe('Improve Digital Adapter Tests', function () { + let idClient = new ImproveDigitalAdServerJSClient('hb'); + + const METHOD = 'GET'; + const URL = '//ad.360yield.com/hb'; + const PARAM_PREFIX = 'jsonp='; const simpleBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012544 - } - } - ] + bidder: 'improvedigital', + params: { + placementId: 1053688 + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239' }; const simpleSmartTagBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - publisherId: 1032, - placementKey: 'data_team_test_hb_smoke_test' - } - } - ] + bidder: 'improvedigital', + bidId: '1a2b3c', + placementCode: 'placement1', + params: { + publisherId: 1032, + placementKey: 'data_team_test_hb_smoke_test' + } }; - const keyValueBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012546, - keyValues: { - hbkv: ['01'] - } - } - } - ] - }; + describe('isBidRequestValid', () => { + it('should return false when no bid', () => { + expect(spec.isBidRequestValid()).to.equal(false); + }); - const sizeBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } - } - } - ] - }; + it('should return false when no bid.params', () => { + let bid = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const twoAdSlots = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012544, - } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } - } - } - ] - }; + it('should return false when both placementId and placementKey + publisherId are missing', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const threeAdSlots = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', + it('should return false when only one of placementKey and publisherId is present', () => { + let bid = { params: { - placementId: 1012544, + publisherId: 1234 } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid = { params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } + placementKey: 'xyz' } - }, - { - bidId: '7g8h9i', - placementCode: 'placement3', - params: { - placementId: 1012546, - keyValues: { - hbkv: ['01'] - } - } - } - ] - }; + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const badRequest1 = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - unknownId: 123456 - } - } - ] - }; + it('should return true when placementId is passed', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(simpleBidRequest)).to.equal(true); + }); - const twoAdSlotsSingleRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - singleRequest: true, - placementId: 1012544, - } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } + it('should return true when both placementKey and publisherId are passed', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(simpleSmartTagBidRequest)).to.equal(true); + }); + }); + + describe('buildRequests', () => { + it('should make a well-formed request objects', () => { + const requests = spec.buildRequests([simpleBidRequest]); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(1); + + const request = requests[0]; + expect(request.method).to.equal(METHOD); + expect(request.url).to.equal(URL); + expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); + + const params = JSON.parse(request.data.substring(PARAM_PREFIX.length)); + expect(params.bid_request).to.be.an('object'); + expect(params.bid_request.id).to.be.a('string'); + expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); + expect(params.bid_request.imp).to.deep.equal([ + { + id: '33e9500b21129f', + pid: 1053688, + tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + banner: {} } - } - ] - }; + ]); + }); - const simpleResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\" { + const requests = spec.buildRequests([simpleSmartTagBidRequest]); + const params = JSON.parse(requests[0].data.substring(PARAM_PREFIX.length)); + expect(params.bid_request.imp[0].pubid).to.equal(1032); + expect(params.bid_request.imp[0].pkey).to.equal('data_team_test_hb_smoke_test'); + }); - const zeroPriceResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 0, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\" { + let bidRequest = Object.assign({}, simpleBidRequest); + const keyValues = { + testKey: [ + 'testValue' + ] + }; + bidRequest.params.keyValues = keyValues; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(request.data.substring(PARAM_PREFIX.length)); + expect(params.bid_request.imp[0].kvw).to.deep.equal(keyValues); + }); - const multipleResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\" { + let bidRequest = Object.assign({}, simpleBidRequest); + const size = { w: 800, - cid: '99005', - adm: 'document.writeln(\" { + const requests = spec.buildRequests([ + simpleBidRequest, + simpleSmartTagBidRequest + ]); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(2); + }); + }); + + describe('interpretResponse', () => { + const serverResponse = { + 'body': { + 'id': '687a06c541d8d1', + 'site_id': 191642, + 'bid': [ + { + 'isNet': false, + 'id': '33e9500b21129f', + 'advid': '5279', + 'price': 1.45888594164456, + 'nurl': 'http://ad.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', + 'h': 290, + 'pid': 1053688, + 'sync': [ + 'http://link1', + 'http://link2' + ], + 'crid': '422031', + 'w': 600, + 'cid': '99006', + 'adm': 'document.writeln(\"\\\"\\\"\\/<\\/a>\");document.writeln(\"<\\/improvedigital_ad_output_information>\");' + } ], - nurl: 'http://nurl2', - h: 600, - pid: 1053687, - crid: '422030', - w: 800, - cid: '99005', - adm: 'document.writeln(\"\\\"\\\"\\/<\\/a>\");document.writeln(\"<\\/improvedigital_ad_output_information>\");' + } ], - nurl: 'http://nurl2', - h: 600, - pid: 1053687, - crid: '422030', - w: 800, - cid: '99005' + 'debug': '' } - ], - debug: '' - }; + }; - const simpleResponseNoSync = { - id: '701903620', - site_id: 191642, - bid: [ + let expectedBid = [ { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"', + 'adId': '33e9500b21129f', + 'creativeId': '422031', + 'cpm': 1.45888594164456, + 'currency': 'USD', + 'height': 290, + 'netRevenue': false, + 'requestId': '33e9500b21129f', + 'width': 600 } - ] - }; + ]; - var randomNumber = 9876543210; - beforeEach(() => { - improveDigitalAdapter = new Adapter(); - sandbox = sinon.sandbox.create(); - sandbox.stub( - utils, - 'getUniqueIdentifierStr', - function() { - var retValue = randomNumber.toString(); - randomNumber++; - return retValue; + let expectedTwoBids = [ + expectedBid[0], + { + 'ad': '', + 'adId': '1234', + 'creativeId': '422033', + 'cpm': 1.23, + 'currency': 'USD', + 'height': 400, + 'netRevenue': true, + 'requestId': '1234', + 'width': 700 } - ); - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - $$PREBID_GLOBAL$$._bidsRequested = []; - }); + ]; - afterEach(() => { - sandbox.restore(); - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); - - describe('callBids simpleBidRequest', () => { - beforeEach(() => { - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(simpleBidRequest); + it('should return a well-formed bid', () => { + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.deep.equal(expectedBid); }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); - describe('callBids simpleSmartTagBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(simpleSmartTagBidRequest); + it('should return two bids', () => { + const bids = spec.interpretResponse(serverResponseTwoBids); + expect(bids).to.deep.equal(expectedTwoBids); }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); - describe('callBids keyValueBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(keyValueBidRequest); - }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + it('should register user syncs', () => { + const registerSyncSpy = sinon.spy(userSync, 'registerSync'); + const bids = spec.interpretResponse(serverResponse); + expect(registerSyncSpy.withArgs('image', 'improvedigital', 'http://link1').calledOnce).to.equal(true); + expect(registerSyncSpy.withArgs('image', 'improvedigital', 'http://link2').calledOnce).to.equal(true); }); - }); - describe('callBids sizeBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(sizeBidRequest); - }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); + it('should set dealId correctly', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + let bids; - describe('callBids twoAdSlots', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(twoAdSlots); - }); - it('should call loadScript twice with correct parameters', () => { - sinon.assert.calledTwice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); + response.body.bid[0].lid = 'xyz'; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.not.exist; - describe('callBids threeAdSlots', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(threeAdSlots); - }); - it('should call loadScript thrice with correct parameters', () => { - sinon.assert.calledThrice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); + response.body.bid[0].lid = 268515; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.equal(268515); - describe('callBids bad request 1', () => { - beforeEach(() => { - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(badRequest1); + response.body.bid[0].lid = { + 1: 268515 + }; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.equal(268515); }); - it('should not call loadScript', () => { - sinon.assert.notCalled(adloader.loadScript); - }); - }); - describe('callBids twoAdSlotsSingleRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(twoAdSlotsSingleRequest); + it('should set currency', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].currency = 'eur'; + const bids = spec.interpretResponse(response); + expect(bids[0].currency).to.equal('EUR'); }); - it('should call loadScript twice with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); - describe('improveDigitalResponse no response', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse([]); - }); - it('should not call bidmanager.addBidResponse', () => { - sinon.assert.notCalled(bidmanager.addBidResponse); - }); - }); - - describe('improveDigitalResponse simpleResponse', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(simpleResponse); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, statusMessage: 'Bid available', ad: '', cpm: 1.85185185185185, adId: '1a2b3c'})); - }); - }); + it('should return empty array for bad response or no price', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + let bids; - describe('improveDigitalResponse zero bid', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(zeroPriceResponse); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, statusMessage: 'Bid returned empty or error response', adId: '1a2b3c'})); - }); - }); + // Price missing or 0 + response.body.bid[0].price = 0; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + delete response.body.bid[0].price; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].price = null; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); - describe('improveDigitalResponse multipleResponseWithOneNoBid', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(twoAdSlots); - improveDigitalAdapter.callBids(twoAdSlots); - $$PREBID_GLOBAL$$.improveDigitalResponse(multipleResponseWithOneNoBid); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledTwice(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, adId: '1a2b3c', statusMessage: 'Bid available', ad: '', cpm: 1.85185185185185})); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement2', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '4d5e6f', statusMessage: 'Bid returned empty or error response'})); - }); - }); + // errorCode present + response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].errorCode = undefined; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); - describe('improveDigitalResponse multipleInvalidResponses', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(twoAdSlots); - improveDigitalAdapter.callBids(twoAdSlots); - $$PREBID_GLOBAL$$.improveDigitalResponse(multipleInvalidResponses); - }); - it('should call bidmanager.addBidResponse twice both with invalid', () => { - sinon.assert.calledTwice(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '1a2b3c', statusMessage: 'Bid returned empty or error response'})); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement2', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '4d5e6f', statusMessage: 'Bid returned empty or error response'})); + // Adm missing or bad + response = JSON.parse(JSON.stringify(serverResponse)); + delete response.body.bid[0].adm; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = null; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = 1234; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = {}; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); }); - }); - describe('improveDigitalResponse simpleResponseNoSync', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(simpleResponseNoSync); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, statusMessage: 'Bid available', ad: '', cpm: 1.85185185185185, adId: '1a2b3c'})); + it('should set netRevenue', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].isNet = true; + const bids = spec.interpretResponse(response); + expect(bids[0].netRevenue).to.equal(true); }); }); }); From 6a0c5cabdbbbe1a45234c84fad33fcb794b13a92 Mon Sep 17 00:00:00 2001 From: PWyrembak Date: Thu, 26 Oct 2017 19:02:59 +0300 Subject: [PATCH 07/22] Migrating TrustX adapter to 1.0 (#1709) * Add trustx adapter and tests for it * update integration example * Update trustx adapter * Post-review fixes of Trustx adapter * Code improvement for trustx adapter: changed default price type from gross to net * Update TrustX adapter to support the 1.0 version * Make requested changes for TrustX adapter * Updated markdown file for TrustX adapter * Fix TrustX adapter and spec file --- modules/trustxBidAdapter.js | 287 ++++++------- modules/trustxBidAdapter.md | 40 ++ test/spec/modules/trustxBidAdapter_spec.js | 469 ++++++++++++--------- 3 files changed, 437 insertions(+), 359 deletions(-) create mode 100755 modules/trustxBidAdapter.md diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index 13f893a841d..f16b8b96ec8 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -1,165 +1,148 @@ -const utils = require('src/utils.js'); -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const adloader = require('src/adloader'); -const adaptermanager = require('src/adaptermanager'); -const CONSTANTS = require('src/constants.json'); - -var TrustxAdapter = function TrustxAdapter() { - const bidderCode = 'trustx'; - const reqHost = '//sofia.trustx.org'; - const reqPath = '/hb?'; - const LOG_ERROR_MESS = { - noAuid: 'Bid from response has no auid parameter - ', - noAdm: 'Bid from response has no adm parameter - ', - noBid: 'Array of bid objects is empty', - noPlacementCode: 'Can\'t find placementCode for bid with auid - ', - havePCodeFor: ', placementCode is available only for the following uids - ', - emptyUids: 'Uids should be not empty', - emptySeatbid: 'Seatbid array from response has empty item', - emptyResponse: 'Response is empty', - hasEmptySeatbidArray: 'Response has empty seatbid array', - hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' - }; +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'trustx'; +const ENDPOINT_URL = '//sofia.trustx.org/hb'; +const TIME_TO_LIVE = 360; +const ADAPTER_SYNC_URL = '//sofia.trustx.org/push_sync'; +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; +export const spec = { + code: BIDDER_CODE, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + const auids = []; + const bidsMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + if (!bidsMap[bid.params.uid]) { + bidsMap[bid.params.uid] = [bid]; + auids.push(bid.params.uid); + } else { + bidsMap[bid.params.uid].push(bid); + } + }); - function _makeHandler(auids, placementMap) { - var cbName = bidderCode + '_callback_wrapper_' + auids.join('_'); - $$PREBID_GLOBAL$$[cbName] = function(resp) { - delete $$PREBID_GLOBAL$$[cbName]; - _responseProcessing(resp, auids, placementMap); + const payload = { + u: utils.getTopWindowUrl(), + pt: priceType, + auids: auids.join(','), }; - return '$$PREBID_GLOBAL$$.' + cbName; - } - function _sendRequest(auids, placementMap) { - var query = []; - var path = reqPath; - query.push('u=' + encodeURIComponent(location.href)); - query.push('auids=' + encodeURIComponent(auids.join(','))); - query.push('cb=' + _makeHandler(auids, placementMap)); - query.push('pt=' + (window.globalPrebidTrustxPriceType === 'gross' ? 'gross' : 'net')); - - adloader.loadScript(reqHost + path + query.join('&')); - } - - function _callBids(params) { - var auids = []; - var placementMap = {}; - var hasBid; - var bid; - var bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - bid = bids[i]; - if (bid && bid.bidder === bidderCode && bid.placementCode) { - hasBid = true; - if (bid.params && bid.params.uid) { - if (!placementMap[bid.params.uid]) { - placementMap[bid.params.uid] = [bid.placementCode]; - auids.push(bid.params.uid); - } else { - placementMap[bid.params.uid].push(bid.placementCode); - } - } - } + return { + method: 'GET', + url: ENDPOINT_URL, + data: payload, + bidsMap: bidsMap, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; } - if (auids.length) { - _sendRequest(auids, placementMap); - } else if (hasBid) { - utils.logError(LOG_ERROR_MESS.emptyUids); + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses); + }); } - } - - function _getBidFromResponse(resp) { - if (!resp) { - utils.logError(LOG_ERROR_MESS.emptySeatbid); - } else if (!resp.bid) { - utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(resp)); - } else if (!resp.bid[0]) { - utils.logError(LOG_ERROR_MESS.noBid); + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: ADAPTER_SYNC_URL + }]; } - return resp && resp.bid && resp.bid[0]; } - - function _forEachPlacement(error, bid, placementCode) { - var bidObject; - if (error) { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bid); - } else { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bid); - bidObject.cpm = bid.price; - bidObject.ad = bid.adm; - bidObject.width = bid.w; - bidObject.height = bid.h; - if (bid.dealid) { - bidObject.dealId = bid.dealid; - } - } - bidObject.bidderCode = bidderCode; - bidmanager.addBidResponse(placementCode, bidObject); +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); } - - function _addBidResponse(bid, auids, placementMap) { - if (!bid) return; - var errorMessage, placementCodes; - if (!bid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(bid); - else { - placementCodes = placementMap.hasOwnProperty(bid.auid) && placementMap[bid.auid]; - if (!placementCodes) { - errorMessage = LOG_ERROR_MESS.noPlacementCode + bid.auid + LOG_ERROR_MESS.havePCodeFor + auids.join(','); - } - } - - if (!errorMessage) { - if (!bid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(bid); - - var l = placementCodes.length; - while (l--) { - _forEachPlacement(errorMessage, bid, placementCodes[l]); - } - - delete placementMap[bid.auid]; - } - - if (errorMessage) { - utils.logError(errorMessage); + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + awaitingBids.forEach(bid => { + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'USD', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + ad: serverBid.adm, + dealId: serverBid.dealid + }; + bidResponses.push(bidResponse); + }); + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; } } - - function _responseProcessing(resp, auids, placementMap) { - var errorMessage; - - if (!resp) errorMessage = LOG_ERROR_MESS.emptyResponse; - else if (resp.seatbid && !resp.seatbid.length) errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; - - if (!errorMessage) { - resp = resp.seatbid || []; - var l = resp.length; - while (l--) { - _addBidResponse(_getBidFromResponse(resp[l]), auids, placementMap); - } - } - - var n, bidObj; - for (var auid in placementMap) { - if (placementMap.hasOwnProperty(auid) && placementMap[auid]) { - n = placementMap[auid].length; - while (n--) { - bidObj = bidfactory.createBid(CONSTANTS.STATUS.NO_BID); - bidObj.bidderCode = bidderCode; - bidmanager.addBidResponse(placementMap[auid][n], bidObj); - } - } - } - - if (errorMessage) utils.logError(errorMessage); + if (errorMessage) { + utils.logError(errorMessage); } +} - return { - callBids: _callBids - }; -}; - -adaptermanager.registerBidAdapter(new TrustxAdapter(), 'trustx'); - -module.exports = TrustxAdapter; +registerBidder(spec); diff --git a/modules/trustxBidAdapter.md b/modules/trustxBidAdapter.md new file mode 100755 index 00000000000..ca407b0c5e8 --- /dev/null +++ b/modules/trustxBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: TrustX Bidder Adapter +Module Type: Bidder Adapter +Maintainer: paul@trustx.org + +# Description + +Module that connects to TrustX demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "trustx", + params: { + uid: '44', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "trustx", + params: { + uid: 45, + priceType: 'gross' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index 7208ebef343..918e03674a9 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -1,234 +1,289 @@ -describe('trustx adapter tests', function () { - var expect = require('chai').expect; - var assert = require('chai').assert; - var urlParse = require('url-parse'); - var querystringify = require('querystringify'); - - var adapter = require('modules/trustxBidAdapter'); - var bidmanager = require('src/bidmanager'); - var adLoader = require('src/adloader'); - var utils = require('src/utils'); - window.$$PREBID_GLOBAL$$ = window.$$PREBID_GLOBAL$$ || {}; - - if (typeof (pbjs) === 'undefined') { - var pbjs = window.$$PREBID_GLOBAL$$; - } - let stubLoadScript; - beforeEach(function () { - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - }); - afterEach(function () { - stubLoadScript.restore(); - }); - var logErrorSpy; - beforeEach(function () { - logErrorSpy = sinon.spy(utils, 'logError'); - }); - afterEach(function () { - logErrorSpy.restore(); +import { expect } from 'chai'; +import { spec } from 'modules/trustxBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('TrustXAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); }); - describe('creation of request url', function () { - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - it('should fix parameter name', function () { - var params = { - bidderCode: 'trustx', - bids: [ - { - bidder: 'trustx', - params: { - uid: 5 - }, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - params: { - uid: 6 - }, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - params: {}, - placementCode: 'div-2' - }, - { - bidder: 'trustx', - params: { - uid: 6, - test: true - }, - placementCode: 'div-3' - }, - { - bidder: 'trustx', - placementCode: 'div-4' - } - ] + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'trustx', + 'params': { + 'uid': '44' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 0 }; - adapter().callBids(params); - var bidUrl = stubLoadScript.getCall(0).args[0]; - sinon.assert.calledWith(stubLoadScript, bidUrl); - var parsedBidUrl = urlParse(bidUrl); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = '$$PREBID_GLOBAL$$.trustx_callback_wrapper_5_6'; - expect(parsedBidUrl.hostname).to.equal('sofia.trustx.org'); - expect(parsedBidUrl.pathname).to.equal('/hb'); - expect(parsedBidUrlQueryString).to.have.property('auids').and.to.equal('5,6'); - expect(parsedBidUrlQueryString).to.have.property('u').and.to.equal(location.href); - expect(parsedBidUrlQueryString).to.have.property('cb').and.to.equal(generatedCallback); + expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('validate incoming params', function () { - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - it('has no correct item in config', function () { - var params = { - bidderCode: 'trustx', - bids: [ - { - bidder: 'trustx', - params: {}, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - placementCode: 'div-1' - } - ] - }; - adapter().callBids(params); - sinon.assert.notCalled(stubLoadScript); - expect(logErrorSpy.getCall(0).args[0]).to.equal('Uids should be not empty'); + + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'trustx', + 'params': { + 'uid': '45' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '42dbe3a7168a6a', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', () => { + const request = spec.buildRequests([bidRequests[0]]); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43'); + }); + + it('auids must not be duplicated', () => { + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43,45'); + }); + + it('pt parameter must be "gross" if params.priceType === "gross"', () => { + bidRequests[1].params.priceType = 'gross'; + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'gross'); + expect(payload).to.have.property('auids', '43,45'); + delete bidRequests[1].params.priceType; + }); + + it('pt parameter must be "net" or "gross"', () => { + bidRequests[1].params.priceType = 'some'; + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43,45'); + delete bidRequests[1].params.priceType; }); }); - describe('handling of the callback response', function () { - if (typeof (pbjs._bidsReceived) === 'undefined') { - pbjs._bidsReceived = []; - } - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - if (typeof (pbjs._adsReceived) === 'undefined') { - pbjs._adsReceived = []; - } - var params = { - bidderCode: 'trustx', - bids: [ + + describe('interpretResponse', () => { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 43, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 44, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0, 'auid': 45, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0, 'adm': '
test content 4
', 'h': 250, 'w': 300}], 'seat': '1'}, + undefined, + {'bid': [], 'seat': '1'}, + {'seat': '1'}, + ]; + + it('should get correct bid response', () => { + const bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should get correct multi bid response', () => { + const bidRequests = [ { - bidder: 'trustx', - params: { - uid: 5 + 'bidder': 'trustx', + 'params': { + 'uid': '43' }, - placementCode: '/19968336/header-bid-tag-0' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71a5b', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', }, { - bidder: 'trustx', - params: { - uid: 6 + 'bidder': 'trustx', + 'params': { + 'uid': '44' }, - placementCode: '/19968336/header-bid-tag-1' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4dff80cc4ee346', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', }, { - bidder: 'trustx', - params: { - uid: 42 + 'bidder': 'trustx', + 'params': { + 'uid': '43' }, - placementCode: '/19968336/header-bid-tag-2' + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '5703af74d0472a', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '300bfeb0d71a5b', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '5703af74d0472a', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, }, { - bidder: 'trustx', - params: { - uid: 43 + 'requestId': '4dff80cc4ee346', + 'cpm': 0.5, + 'creativeId': 44, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '
test content 2
', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0], responses[1]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('handles wrong and nobid responses', () => { + const bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '45' }, - placementCode: '/19968336/header-bid-tag-3' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d7190gf', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', }, { - bidder: 'trustx', - params: { - uid: 44 + 'bidder': 'trustx', + 'params': { + 'uid': '46' }, - placementCode: '/19968336/header-bid-tag-4' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71321', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', }, { - bidder: 'trustx', - params: { - uid: 45 + 'bidder': 'trustx', + 'params': { + 'uid': '50' }, - placementCode: '/19968336/header-bid-tag-5' + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '300bfeb0d7183bb', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', } - ] - }; - it('callback function should exist', function () { - adapter().callBids(params); - expect(pbjs['trustx_callback_wrapper_5_6_42_43_44_45']) - .to.exist.and.to.be.a('function'); - }); - it('bidmanager.addBidResponse should be called with correct arguments', function () { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - adapter().callBids(params); - var adUnits = []; - var unit = {}; - unit.bids = params.bids; - unit.code = '/19968336/header-bid-tag'; - adUnits.push(unit); - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = [params]; - } else { - pbjs._bidsRequested.push(params); - } - pbjs.adUnits = adUnits; - var response = { - seatbid: [ - {bid: [{price: 1.15, adm: '
test content 1
', auid: 5, h: 90, w: 728}], seat: '1'}, - {bid: [{price: 0, auid: 6, h: 250, w: 300}], seat: '1'}, - {bid: [{price: 0, adm: '
test content 3
', h: 250, w: 300}], seat: '1'}, - undefined, - {bid: [], seat: '1'}, - {seat: '1'}, - {bid: [{price: 0, adm: '
test content 7
', auid: 46, h: 250, w: 300}], seat: '1'} - ] - }; - pbjs['trustx_callback_wrapper_5_6_42_43_44_45'](response); - var bidPlacementCode1 = stubAddBidResponse.getCall(1).args[0]; - var bidObject1 = stubAddBidResponse.getCall(1).args[1]; - var bidPlacementCode2 = stubAddBidResponse.getCall(0).args[0]; - var bidObject2 = stubAddBidResponse.getCall(0).args[1]; - var bidPlacementCode3 = stubAddBidResponse.getCall(2).args[0]; - var bidObject3 = stubAddBidResponse.getCall(2).args[1]; - var bidPlacementCode4 = stubAddBidResponse.getCall(3).args[0]; - var bidObject4 = stubAddBidResponse.getCall(3).args[1]; - var bidPlacementCode5 = stubAddBidResponse.getCall(4).args[0]; - var bidObject5 = stubAddBidResponse.getCall(4).args[1]; - var bidPlacementCode6 = stubAddBidResponse.getCall(5).args[0]; - var bidObject6 = stubAddBidResponse.getCall(5).args[1]; - expect(logErrorSpy.getCall(5).args[0]).to.equal('Bid from response has no adm parameter - {"price":0,"auid":6,"h":250,"w":300}'); - expect(logErrorSpy.getCall(4).args[0]).to.equal('Bid from response has no auid parameter - {"price":0,"adm":"<' + 'div>test content 3","h":250,"w":300}'); - expect(logErrorSpy.getCall(3).args[0]).to.equal('Seatbid array from response has empty item'); - expect(logErrorSpy.getCall(2).args[0]).to.equal('Array of bid objects is empty'); - expect(logErrorSpy.getCall(1).args[0]).to.equal('Seatbid from response has no array of bid objects - {"seat":"1"}'); - expect(logErrorSpy.getCall(0).args[0]).to.equal('Can\'t find placementCode for bid with auid - 46, placementCode is available only for the following uids - 5,6,42,43,44,45'); - expect(bidPlacementCode1).to.equal('/19968336/header-bid-tag-0'); - expect(bidObject1.cpm).to.equal(1.15); - expect(bidObject1.ad).to.equal('
test content 1
'); - expect(bidObject1.width).to.equal(728); - expect(bidObject1.height).to.equal(90); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('trustx'); - expect(bidPlacementCode2).to.equal('/19968336/header-bid-tag-1'); - expect(bidObject2.getStatusCode()).to.equal(2); - expect(bidPlacementCode3).to.equal('/19968336/header-bid-tag-2'); - expect(bidObject3.getStatusCode()).to.equal(2); - expect(bidPlacementCode4).to.equal('/19968336/header-bid-tag-3'); - expect(bidObject4.getStatusCode()).to.equal(2); - expect(bidPlacementCode5).to.equal('/19968336/header-bid-tag-4'); - expect(bidObject5.getStatusCode()).to.equal(2); - expect(bidPlacementCode6).to.equal('/19968336/header-bid-tag-5'); - expect(bidObject6.getStatusCode()).to.equal(2); - stubAddBidResponse.restore(); + ]; + const request = spec.buildRequests(bidRequests); + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(2)}}, request); + expect(result.length).to.equal(0); }); }); }); From caec5c0022405fada40562e6483abc7f92856d6a Mon Sep 17 00:00:00 2001 From: dbemiller Date: Thu, 26 Oct 2017 12:03:44 -0400 Subject: [PATCH 08/22] Fix test-coverage bug (#1765) * Fixed a bug in test-coverage single-file tests. * Reverted unintended changes. --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index d7a151ce357..7b05b22ad06 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -189,7 +189,7 @@ gulp.task('test', ['clean'], function (done) { // If --file "" is given, the task will only run tests in the specified file. gulp.task('test-coverage', ['clean'], function(done) { - new KarmaServer(karmaConfMaker(true, false, argv.file), newKarmaCallback(done)).start(); + new KarmaServer(karmaConfMaker(true, false, false, argv.file), newKarmaCallback(done)).start(); }); // View the code coverage report in the browser. From 9a6ddc28313d9a0a09e1153fd9465ee52bd0e6a3 Mon Sep 17 00:00:00 2001 From: Paul Yang Date: Thu, 26 Oct 2017 09:31:51 -0700 Subject: [PATCH 09/22] Update Conversant adapter to Prebid 1.0 (#1711) * Conversant adapter initial support for prebid 1.0 * Add video support for conversant adapter * Add tests and md * Update conversant contact address * Return data object in buildRequests without converting it to a string * Conversant adapter initial support for prebid 1.0 * Add video support for conversant adapter * Add tests and md * Update conversant contact address * Return data object in buildRequests without converting it to a string * Better validation for site id * Switch to use utils._each and utils._map * Add tests for displaymanagerver * Review changes for conversant --- modules/conversantBidAdapter.js | 458 +++++++------ modules/conversantBidAdapter.md | 41 ++ .../spec/modules/conversantBidAdapter_spec.js | 613 ++++++++---------- 3 files changed, 517 insertions(+), 595 deletions(-) create mode 100644 modules/conversantBidAdapter.md diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index d51008559f2..7e71d3be8aa 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,99 +1,70 @@ -'use strict'; -var VERSION = '2.1.0'; -var CONSTANTS = require('src/constants.json'); -var utils = require('src/utils.js'); -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader'); -var ajax = require('src/ajax').ajax; -var adaptermanager = require('src/adaptermanager'); +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import { VIDEO } from 'src/mediaTypes'; + +const BIDDER_CODE = 'conversant'; +const URL = '//media.msg.dotomi.com/s2s/header/24'; +const SYNC_URL = '//media.msg.dotomi.com/w/user.sync'; +const VERSION = '2.2.0'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['cnvr'], // short code + supportedMediaTypes: [VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid - The bid params to validate. + * @return {boolean} True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (!bid || !bid.params) { + utils.logWarn(BIDDER_CODE + ': Missing bid parameters'); + return false; + } -/** - * Adapter for requesting bids from Conversant - */ -var ConversantAdapter = function () { - var w = window; - var n = navigator; - - // production endpoint - var conversantUrl = '//media.msg.dotomi.com/s2s/header/24?callback=$$PREBID_GLOBAL$$.conversantResponse'; - - // SSAPI returns JSONP with window.pbjs.conversantResponse as the cb - var appendScript = function (code) { - var script = document.createElement('script'); - script.type = 'text/javascript'; - script.className = 'cnvr-response'; - - try { - script.appendChild(document.createTextNode(code)); - document.getElementsByTagName('head')[0].appendChild(script); - } catch (e) { - script.text = code; - document.getElementsByTagName('head')[0].appendChild(script); + if (!utils.isStr(bid.params.site_id)) { + utils.logWarn(BIDDER_CODE + ': site_id must be specified as a string') + return false; } - }; - var getDNT = function () { - return n.doNotTrack === '1' || w.doNotTrack === '1' || n.msDoNotTrack === '1' || n.doNotTrack === 'yes'; - }; + if (isVideoRequest(bid)) { + if (!bid.params.mimes) { + // Give a warning but let it pass + utils.logWarn(BIDDER_CODE + ': mimes should be specified for videos'); + } else if (!utils.isArray(bid.params.mimes) || !bid.params.mimes.every(s => utils.isStr(s))) { + utils.logWarn(BIDDER_CODE + ': mimes must be an array of strings'); + return false; + } + } - var getDevice = function () { - const language = n.language ? 'language' : 'userLanguage'; - return { - h: screen.height, - w: screen.width, - dnt: getDNT() ? 1 : 0, - language: n[language].split('-')[0], - make: n.vendor ? n.vendor : '', - ua: n.userAgent - }; - }; + return true; + }, - var callBids = function (params) { - var conversantBids = params.bids || []; - requestBids(conversantBids); - }; + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + const loc = utils.getTopWindowLocation(); + const page = loc.pathname + loc.search + loc.hash; + const isPageSecure = (loc.protocol === 'https:') ? 1 : 0; + let siteId = ''; + let requestId = ''; - var requestBids = function (bidReqs) { - // build bid request object - var page = location.pathname + location.search + location.hash; - var siteId = ''; - var conversantImps = []; - var conversantBidReqs; - var secure = 0; - - // build impression array for conversant - utils._each(bidReqs, function (bid) { - var bidfloor = utils.getBidIdParameter('bidfloor', bid.params); - var adW = 0; - var adH = 0; - var format; - var tagId; - var pos; - var imp; - - secure = utils.getBidIdParameter('secure', bid.params) ? 1 : secure; - siteId = utils.getBidIdParameter('site_id', bid.params) + ''; - tagId = utils.getBidIdParameter('tag_id', bid.params); - pos = utils.getBidIdParameter('position', bid.params); - - // Allow sizes to be overridden per placement - var bidSizes = Array.isArray(bid.params.sizes) ? bid.params.sizes : bid.sizes; - - if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { - adW = bidSizes[0]; - adH = bidSizes[1]; - } else { - format = []; - utils._each(bidSizes, function (bidSize) { - format.push({ - w: bidSize[0], - h: bidSize[1] - }); - }); - } + const conversantImps = validBidRequests.map(function(bid) { + const bidfloor = utils.getBidIdParameter('bidfloor', bid.params); + const secure = isPageSecure || (utils.getBidIdParameter('secure', bid.params) ? 1 : 0); + + siteId = utils.getBidIdParameter('site_id', bid.params); + requestId = bid.requestId; + + const format = convertSizes(bid.sizes); - imp = { + const imp = { id: bid.bidId, secure: secure, bidfloor: bidfloor || 0, @@ -101,178 +72,187 @@ var ConversantAdapter = function () { displaymanagerver: VERSION }; - if (tagId !== '') { - imp.tagid = tagId; - } - - if (bid.mediaType === 'video') { - var mimes = []; - var maxduration = 0; - var protocols = []; - var api = []; - - var video = Array.isArray(format) ? {format: format} : {w: adW, h: adH}; - - mimes = utils.getBidIdParameter('mimes', bid.params); - if (mimes !== '') { - video.mimes = mimes; - } + copyOptProperty(bid.params, 'tag_id', imp, 'tagid'); - maxduration = utils.getBidIdParameter('maxduration', bid.params); - if (maxduration !== '') { - video.maxduration = maxduration; - } + if (isVideoRequest(bid)) { + const video = {format: format}; - protocols = utils.getBidIdParameter('protocols', bid.params); - if (protocols !== '') { - video.protocols = protocols; - } - - api = utils.getBidIdParameter('api', bid.params); - if (api !== '') { - video.api = api; - } - - if (pos !== '') { - video.pos = pos; - } + copyOptProperty(bid.params, 'position', video, 'pos'); + copyOptProperty(bid.params, 'mimes', video); + copyOptProperty(bid.params, 'maxduration', video); + copyOptProperty(bid.params, 'protocols', video); + copyOptProperty(bid.params, 'api', video); imp.video = video; } else { - var banner = Array.isArray(format) ? {format: format} : {w: adW, h: adH}; + const banner = {format: format}; + + copyOptProperty(bid.params, 'position', banner, 'pos'); - if (pos !== '') { - banner.pos = pos; - } imp.banner = banner; } - conversantImps.push(imp); + return imp; }); - conversantBidReqs = { - 'id': utils.getUniqueIdentifierStr(), - 'imp': conversantImps, - - 'site': { - 'id': siteId, - 'mobile': document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, - 'page': page + const payload = { + id: requestId, + imp: conversantImps, + site: { + id: siteId, + mobile: document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, + page: page }, - - 'device': getDevice(), - 'at': 1 + device: getDevice(), + at: 1 }; - var url = secure ? 'https:' + conversantUrl : location.protocol + conversantUrl; - ajax(url, appendScript, JSON.stringify(conversantBidReqs), { - withCredentials: true - }); - }; + return { + method: 'POST', + url: URL, + data: payload, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + const requestMap = {}; + serverResponse = serverResponse.body; + + if (bidRequest && bidRequest.data && bidRequest.data.imp) { + utils._each(bidRequest.data.imp, imp => requestMap[imp.id] = imp); + } - var addEmptyBidResponses = function (placementsWithBidsBack) { - var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); - - if (allConversantBidRequests && allConversantBidRequests.bids) { - utils._each(allConversantBidRequests.bids, function (conversantBid) { - if (!utils.contains(placementsWithBidsBack, conversantBid.placementCode)) { - // Add a no-bid response for this placement. - var bid = bidfactory.createBid(2, conversantBid); - bid.bidderCode = 'conversant'; - bidmanager.addBidResponse(conversantBid.placementCode, bid); - } + if (serverResponse && utils.isArray(serverResponse.seatbid)) { + utils._each(serverResponse.seatbid, function(bidList) { + utils._each(bidList.bid, function(conversantBid) { + const responseCPM = parseFloat(conversantBid.price); + if (responseCPM > 0.0 && conversantBid.impid) { + const responseAd = conversantBid.adm || ''; + const responseNurl = conversantBid.nurl || ''; + const request = requestMap[conversantBid.impid]; + + const bid = { + requestId: conversantBid.impid, + currency: serverResponse.cur || 'USD', + cpm: responseCPM, + creativeId: conversantBid.crid || '' + }; + + if (request.video) { + bid.vastUrl = responseAd; + bid.mediaType = 'video'; + + if (request.video.format.length >= 1) { + bid.width = request.video.format[0].w; + bid.height = request.video.format[0].h; + } + } else { + bid.ad = responseAd + ''; + bid.width = conversantBid.w; + bid.height = conversantBid.h; + } + + bidResponses.push(bid); + } + }) }); } - }; - var parseSeatbid = function (bidResponse) { - var placementsWithBidsBack = []; - utils._each(bidResponse.bid, function (conversantBid) { - var responseCPM; - var placementCode = ''; - var id = conversantBid.impid; - var bid = {}; - var responseAd; - var responseNurl; - var sizeArrayLength; - - // Bid request we sent Conversant - var bidRequested = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant').bids.find(bid => bid.bidId === id); - - if (bidRequested) { - placementCode = bidRequested.placementCode; - bidRequested.status = CONSTANTS.STATUS.GOOD; - responseCPM = parseFloat(conversantBid.price); - - if (responseCPM !== 0.0) { - conversantBid.placementCode = placementCode; - placementsWithBidsBack.push(placementCode); - conversantBid.size = bidRequested.sizes; - responseAd = conversantBid.adm || ''; - responseNurl = conversantBid.nurl || ''; - - // Our bid! - bid = bidfactory.createBid(1, bidRequested); - bid.creative_id = conversantBid.id || ''; - bid.bidderCode = 'conversant'; - bid.cpm = responseCPM; - - if (bidRequested.mediaType === 'video') { - bid.vastUrl = responseAd; - } else { - // Track impression image onto returned html - bid.ad = responseAd + ''; - } + return bidResponses; + }, + + /** + * Return use sync info + * + * @param {SyncOptions} syncOptions - Info about usersyncs that the adapter should obey + * @return {UserSync} Adapter sync type and url + */ + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: SYNC_URL + }]; + } + } +}; - sizeArrayLength = bidRequested.sizes.length; - if (sizeArrayLength === 2 && typeof bidRequested.sizes[0] === 'number' && typeof bidRequested.sizes[1] === 'number') { - bid.width = bidRequested.sizes[0]; - bid.height = bidRequested.sizes[1]; - } else { - bid.width = bidRequested.sizes[0][0]; - bid.height = bidRequested.sizes[0][1]; - } +/** + * Determine do-not-track state + * + * @returns {boolean} + */ +function getDNT() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} - bidmanager.addBidResponse(placementCode, bid); - } - } - }); - addEmptyBidResponses(placementsWithBidsBack); +/** + * Return openrtb device object that includes ua, width, and height. + * + * @returns {Device} Openrtb device object + */ +function getDevice() { + const language = navigator.language ? 'language' : 'userLanguage'; + return { + h: screen.height, + w: screen.width, + dnt: getDNT() ? 1 : 0, + language: navigator[language].split('-')[0], + make: navigator.vendor ? navigator.vendor : '', + ua: navigator.userAgent }; +} - // Register our callback to the global object: - $$PREBID_GLOBAL$$.conversantResponse = function (conversantResponseObj, path) { - // valid object? - if (conversantResponseObj && conversantResponseObj.id) { - if (conversantResponseObj.seatbid && conversantResponseObj.seatbid.length > 0 && conversantResponseObj.seatbid[0].bid && conversantResponseObj.seatbid[0].bid.length > 0) { - utils._each(conversantResponseObj.seatbid, parseSeatbid); - } else { - // no response data for any placements - addEmptyBidResponses([]); - } - } else { - // no response data for any placements - addEmptyBidResponses([]); - } - // for debugging purposes - if (path) { - adloader.loadScript(path, function () { - var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); - - if ($$PREBID_GLOBAL$$.conversantDebugResponse) { - $$PREBID_GLOBAL$$.conversantDebugResponse(allConversantBidRequests); - } - }); - } - }; // conversantResponse +/** + * Convert arrays of widths and heights to an array of objects with w and h properties. + * + * [[300, 250], [300, 600]] => [{w: 300, h: 250}, {w: 300, h: 600}] + * + * @param {number[2][]|number[2]} bidSizes - arrays of widths and heights + * @returns {object[]} Array of objects with w and h + */ +function convertSizes(bidSizes) { + let format; - return { - callBids: callBids - }; -}; + if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { + format = [{w: bidSizes[0], h: bidSizes[1]}]; + } else { + format = utils._map(bidSizes, d => { return {w: d[0], h: d[1]}; }); + } -adaptermanager.registerBidAdapter(new ConversantAdapter(), 'conversant', { - supportedMediaTypes: ['video'] -}); + return format; +} -module.exports = ConversantAdapter; +/** + * Check if it's a video bid request + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {boolean} True if it's a video bid + */ +function isVideoRequest(bid) { + return bid.mediaType === 'video' || !!utils.deepAccess(bid, 'mediaTypes.video'); +} + +/** + * Copy property if exists from src to dst + * + * @param {object} src + * @param {string} srcName + * @param {object} dst + * @param {string} [dstName] - Optional. If not specified then srcName is used. + */ +function copyOptProperty(src, srcName, dst, dstName) { + dstName = dstName || srcName; + const obj = utils.getBidIdParameter(srcName, src); + if (obj !== '') { + dst[dstName] = obj; + } +} + +registerBidder(spec); diff --git a/modules/conversantBidAdapter.md b/modules/conversantBidAdapter.md new file mode 100644 index 00000000000..1afdad6d544 --- /dev/null +++ b/modules/conversantBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +- Module Name: Conversant Bidder Adapter +- Module Type: Bidder Adapter +- Maintainer: mediapsr@conversantmedia.com + +# Description + +Module that connects to Conversant's demand sources. Supports banners and videos. + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-test-div', + sizes: [[300, 250]], + bids: [{ + bidder: "conversant", + params: { + site_id: '108060' + } + }] + },{ + code: 'video-test-div', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream' + } + }, + bids: [{ + bidder: "conversant", + params: { + site_id: '88563', + api: [2], + protocols: [1, 2], + mimes: ['video/mp4'] + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 57cd9411e66..81da6867132 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -1,376 +1,277 @@ -var expect = require('chai').expect; +import {expect} from 'chai'; +import {spec} from 'modules/conversantBidAdapter'; +import * as utils from 'src/utils'; + var Adapter = require('modules/conversantBidAdapter'); var bidManager = require('src/bidmanager'); -describe('Conversant adapter tests', function () { - var addBidResponseSpy; - var adapter; - - var bidderRequest = { - bidderCode: 'conversant', - bids: [ - { - bidId: 'bidId1', - bidder: 'conversant', - placementCode: 'div1', - sizes: [[300, 600]], - params: { - site_id: '87293', - position: 1, - tag_id: 'tagid-1', - secure: false - } - }, { - bidId: 'bidId2', - bidder: 'conversant', - placementCode: 'div2', - sizes: [[300, 600]], - params: { - site_id: '87293', - secure: false - } - }, { - bidId: 'bidId3', - bidder: 'conversant', - placementCode: 'div3', - sizes: [[300, 600], [160, 600]], - params: { - site_id: '87293', - position: 1, - tag_id: '', - secure: false +describe('Conversant adapter tests', function() { + const siteId = '108060'; + + const bidRequests = [ + { + bidder: 'conversant', + params: { + site_id: siteId, + position: 1, + tag_id: 'tagid-1', + secure: false, + bidfloor: 0.5 + }, + placementCode: 'pcode000', + transactionId: 'tx000', + sizes: [[300, 250]], + bidId: 'bid000', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + secure: false + }, + placementCode: 'pcode001', + transactionId: 'tx001', + sizes: [[468, 60]], + bidId: 'bid001', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + position: 2, + tag_id: '', + secure: false + }, + placementCode: 'pcode002', + transactionId: 'tx002', + sizes: [[300, 600], [160, 600]], + bidId: 'bid002', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + api: [2], + protocols: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30 + }, + mediaTypes: { + video: { + context: 'instream' } - }, { - bidId: 'bidId4', - bidder: 'conversant', - placementCode: 'div4', - mediaType: 'video', - sizes: [[480, 480]], - params: { - site_id: '89192', - pos: 1, - tagid: 'tagid-4', - secure: false - } - } - ] - }; - - it('The Conversant response should exist and be a function', function () { - expect($$PREBID_GLOBAL$$.conversantResponse).to.exist.and.to.be.a('function'); - }); - - describe('Should submit bid responses correctly', function () { - beforeEach(function () { - addBidResponseSpy = sinon.stub(bidManager, 'addBidResponse'); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter = new Adapter(); - }); - - afterEach(function () { - addBidResponseSpy.restore(); - }); - - it('Should correctly submit valid and empty bids to the bid manager', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId1', - price: 0 - }, { - id: 2345, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - // in this case, the valid bid (div2) is submitted before the empty bids (div1, div3) - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - - expect(firstBid.getStatusCode()).to.equal(1); - expect(firstBid.bidderCode).to.equal('conversant'); - expect(firstBid.cpm).to.equal(0.22); - expect(firstBid.ad).to.equal('adm2' + ''); - expect(placementCode1).to.equal('div2'); - - expect(secondBid.getStatusCode()).to.equal(2); - expect(secondBid.bidderCode).to.equal('conversant'); - expect(placementCode2).to.equal('div1'); - - expect(thirdBid.getStatusCode()).to.equal(2); - expect(thirdBid.bidderCode).to.equal('conversant'); - expect(placementCode3).to.equal('div3'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit bids with statuses of 2 to the bid manager for empty bid responses', function () { - $$PREBID_GLOBAL$$.conversantResponse({id: 1, seatbid: []}); - - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - - expect(placementCode1).to.equal('div1'); - expect(firstBid.getStatusCode()).to.equal(2); - expect(firstBid.bidderCode).to.equal('conversant'); - - expect(placementCode2).to.equal('div2'); - expect(secondBid.getStatusCode()).to.equal(2); - expect(secondBid.bidderCode).to.equal('conversant'); - - expect(placementCode3).to.equal('div3'); - expect(thirdBid.getStatusCode()).to.equal(2); - expect(thirdBid.bidderCode).to.equal('conversant'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit valid bids to the bid manager', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, - w: 300, - ext: {} - }, { - id: 2345, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 - }, { - id: 33333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - - expect(firstBid.getStatusCode()).to.equal(1); - expect(firstBid.bidderCode).to.equal('conversant'); - expect(firstBid.cpm).to.equal(0.11); - expect(firstBid.ad).to.equal('adm' + ''); - expect(placementCode1).to.equal('div1'); - - expect(secondBid.getStatusCode()).to.equal(1); - expect(secondBid.bidderCode).to.equal('conversant'); - expect(secondBid.cpm).to.equal(0.22); - expect(secondBid.ad).to.equal('adm2' + ''); - expect(placementCode2).to.equal('div2'); - - expect(thirdBid.getStatusCode()).to.equal(1); - expect(thirdBid.bidderCode).to.equal('conversant'); - expect(thirdBid.cpm).to.equal(0.33); - expect(thirdBid.ad).to.equal('adm3' + ''); - expect(placementCode3).to.equal('div3'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit video bid responses correctly.', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId4', - price: 0.11, - nurl: 'imp_tracker', - adm: 'vasturl' - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - var videoBid = addBidResponseSpy.getCall(0).args[1]; - var placementCode = addBidResponseSpy.getCall(0).args[0]; - - expect(videoBid.getStatusCode()).to.equal(1); - expect(videoBid.bidderCode).to.equal('conversant'); - expect(videoBid.cpm).to.equal(0.11); - expect(videoBid.vastUrl).to.equal('vasturl'); - expect(placementCode).to.equal('div4'); - }) - }); - - describe('Should submit the correct headers in the xhr', function () { - var server, - adapter; - - var bidResponse = { - id: 123, + }, + placementCode: 'pcode003', + transactionId: 'tx003', + sizes: [640, 480], + bidId: 'bid003', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }]; + + const bidResponses = { + body: { + id: 'req000', seatbid: [{ bid: [{ - id: 1111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, + nurl: 'notify000', + adm: 'markup000', + crid: '1000', + impid: 'bid000', + price: 0.99, w: 300, - ext: {} + h: 250, + adomain: ['https://example.com'], + id: 'bid000' }, { - id: 2222, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 + impid: 'bid001', + price: 0.00000, + id: 'bid001' }, { - id: 3333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 - }] - }] - }; - - beforeEach(function () { - server = sinon.fakeServer.create(); - adapter = new Adapter(); - }); - - afterEach(function () { - server.restore(); - }); - - beforeEach(function () { - var resp = [200, {'Content-type': 'text/javascript'}, '$$PREBID_GLOBAL$$.conversantResponse(\'' + JSON.stringify(bidResponse) + '\')']; - server.respondWith('POST', new RegExp('media.msg.dotomi.com/s2s/header'), resp); - }); - - it('Should contain valid request header properties', function () { - adapter.callBids(bidderRequest); - server.respond(); - - var request = server.requests[0]; - expect(request.requestBody).to.not.be.empty; - }); - }); - describe('Should create valid bid requests.', function () { - var server, - adapter; - - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, + nurl: 'notify002', + adm: 'markup002', + crid: '1002', + impid: 'bid002', + price: 2.99, w: 300, - ext: {} - }, { - id: 2222, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 + h: 600, + adomain: ['https://example.com'], + id: 'bid002' }, { - id: 3333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 + nurl: 'notify003', + adm: 'markup003', + crid: '1003', + impid: 'bid003', + price: 3.99, + adomain: ['https://example.com'], + id: 'bid003' }] }] - }; - - beforeEach(function () { - server = sinon.fakeServer.create(); - adapter = new Adapter(); - }); - - afterEach(function () { - server.restore(); - }); + }, + headers: {}}; + + it('Verify basic properties', function() { + expect(spec.code).to.equal('conversant'); + expect(spec.aliases).to.be.an('array').with.lengthOf(1); + expect(spec.aliases[0]).to.equal('cnvr'); + expect(spec.supportedMediaTypes).to.be.an('array').with.lengthOf(1); + expect(spec.supportedMediaTypes[0]).to.equal('video'); + }); - beforeEach(function () { - var resp = [200, {'Content-type': 'text/javascript'}, '$$PREBID_GLOBAL$$.conversantResponse(\'' + JSON.stringify(bidResponse) + '\')']; - server.respondWith('POST', new RegExp('media.msg.dotomi.com/s2s/header'), resp); - }); + it('Verify user syncs', function() { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({iframeEnabled: true})).to.be.undefined; + expect(spec.getUserSyncs({pixelEnabled: false})).to.be.undefined; - it('Should create valid bid requests.', function () { - adapter.callBids(bidderRequest); - server.respond(); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[0].banner.format[0].w).to.equal(300); - expect(request.imp[0].banner.format[0].h).to.equal(600); - expect(request.imp[0].tagid).to.equal('tagid-1'); - expect(request.imp[0].banner.pos).to.equal(1); - expect(request.imp[0].secure).to.equal(0); - expect(request.site.id).to.equal('89192'); - }); + const syncs = spec.getUserSyncs({pixelEnabled: true}); + expect(syncs).to.be.an('array').with.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('//media.msg.dotomi.com/w/user.sync'); + }); - it('Should not pass empty or missing optional parameters on requests.', function () { - adapter.callBids(bidderRequest); - server.respond(); + it('Verify isBidRequestValid', function() { + expect(spec.isBidRequestValid({})).to.be.false; + expect(spec.isBidRequestValid({params: {}})).to.be.false; + expect(spec.isBidRequestValid({params: {site_id: '123'}})).to.be.true; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[1])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[2])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[3])).to.be.true; + + const simpleVideo = JSON.parse(JSON.stringify(bidRequests[3])); + simpleVideo.params.site_id = 123; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + simpleVideo.params.site_id = siteId; + simpleVideo.params.mimes = [1, 2, 3]; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + simpleVideo.params.mimes = 'bad type'; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + delete simpleVideo.params.mimes; + expect(spec.isBidRequestValid(simpleVideo)).to.be.true; + }); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[1].tagid).to.equal(undefined); - expect(request.imp[2].tagid).to.equal(undefined); - expect(request.imp[1].pos).to.equal(undefined); - }); + it('Verify buildRequest', function() { + const request = spec.buildRequests(bidRequests); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('//media.msg.dotomi.com/s2s/header/24'); + const payload = request.data; + + expect(payload).to.have.property('id', 'req000'); + expect(payload).to.have.property('at', 1); + expect(payload).to.have.property('imp'); + expect(payload.imp).to.be.an('array').with.lengthOf(4); + + expect(payload.imp[0]).to.have.property('id', 'bid000'); + expect(payload.imp[0]).to.have.property('secure', 0); + expect(payload.imp[0]).to.have.property('bidfloor', 0.5); + expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); + expect(payload.imp[0]).to.have.property('banner'); + expect(payload.imp[0].banner).to.have.property('pos', 1); + expect(payload.imp[0].banner).to.have.property('format'); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + expect(payload.imp[0]).to.not.have.property('video'); + + expect(payload.imp[1]).to.have.property('id', 'bid001'); + expect(payload.imp[1]).to.have.property('secure', 0); + expect(payload.imp[1]).to.have.property('bidfloor', 0); + expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[1]).to.not.have.property('tagid'); + expect(payload.imp[1]).to.have.property('banner'); + expect(payload.imp[1].banner).to.not.have.property('pos'); + expect(payload.imp[1].banner).to.have.property('format'); + expect(payload.imp[1].banner.format).to.deep.equal([{w: 468, h: 60}]); + + expect(payload.imp[2]).to.have.property('id', 'bid002'); + expect(payload.imp[2]).to.have.property('secure', 0); + expect(payload.imp[2]).to.have.property('bidfloor', 0); + expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner).to.have.property('pos', 2); + expect(payload.imp[2].banner).to.have.property('format'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); + + expect(payload.imp[3]).to.have.property('id', 'bid003'); + expect(payload.imp[3]).to.have.property('secure', 0); + expect(payload.imp[3]).to.have.property('bidfloor', 0); + expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[3]).to.not.have.property('tagid'); + expect(payload.imp[3]).to.have.property('video'); + expect(payload.imp[3].video).to.not.have.property('pos'); + expect(payload.imp[3].video).to.have.property('format'); + expect(payload.imp[3].video.format).to.deep.equal([{w: 640, h: 480}]); + expect(payload.imp[3].video).to.have.property('mimes'); + expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[3].video).to.have.property('protocols'); + expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); + expect(payload.imp[3].video).to.have.property('api'); + expect(payload.imp[3].video.api).to.deep.equal([2]); + expect(payload.imp[3].video).to.have.property('maxduration', 30); + expect(payload.imp[3]).to.not.have.property('banner'); + + expect(payload).to.have.property('site'); + expect(payload.site).to.have.property('id', siteId); + expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); + const loc = utils.getTopWindowLocation(); + const page = loc.pathname + loc.search + loc.hash; + expect(payload.site).to.have.property('page', page); + + expect(payload).to.have.property('device'); + expect(payload.device).to.have.property('w', screen.width); + expect(payload.device).to.have.property('h', screen.height); + expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); + expect(payload.device).to.have.property('ua', navigator.userAgent); + }); - it('Should create the format objects correctly.', function () { - adapter.callBids(bidderRequest); - server.respond(); + it('Verify interpretResponse', function() { + const request = spec.buildRequests(bidRequests); + const response = spec.interpretResponse(bidResponses, request); + expect(response).to.be.an('array').with.lengthOf(3); + + let bid = response[0]; + expect(bid).to.have.property('requestId', 'bid000'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 0.99); + expect(bid).to.have.property('creativeId', '1000'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 250); + expect(bid).to.have.property('ad', 'markup000'); + + // There is no bid001 because cpm is $0 + + bid = response[1]; + expect(bid).to.have.property('requestId', 'bid002'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 2.99); + expect(bid).to.have.property('creativeId', '1002'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 600); + expect(bid).to.have.property('ad', 'markup002'); + + bid = response[2]; + expect(bid).to.have.property('requestId', 'bid003'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 3.99); + expect(bid).to.have.property('creativeId', '1003'); + expect(bid).to.have.property('width', 640); + expect(bid).to.have.property('height', 480); + expect(bid).to.have.property('vastUrl', 'markup003'); + expect(bid).to.have.property('mediaType', 'video'); + }); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[2].banner.format.length).to.equal(2); - expect(request.imp[2].banner.format[0].w).to.equal(300); - expect(request.imp[2].banner.format[1].w).to.equal(160); - }); + it('Verify handling of bad responses', function() { + let response = spec.interpretResponse({}, {}); + expect(response).to.be.an('array').with.lengthOf(0); + response = spec.interpretResponse({id: '123'}, {}); + expect(response).to.be.an('array').with.lengthOf(0); + response = spec.interpretResponse({id: '123', seatbid: []}, {}); + expect(response).to.be.an('array').with.lengthOf(0); }); -}); +}) From edc22740c37f0e0f5eaf0f28498ada6a8aa8d45d Mon Sep 17 00:00:00 2001 From: Jacek Drobiecki Date: Thu, 26 Oct 2017 18:38:40 +0200 Subject: [PATCH 10/22] Add AdOcean adapter (#1735) * Initial revision of adocean bid adapter (ADOCEAN-13634, ADOCEAN-13635) * Minor fixes * new demo placement * formating after lint * move request parameters to params * adocean adpater tests * minor fixes * added ttl, netRevenue nad creativeId. merged with upstream --- modules/adoceanBidAdapter.js | 101 +++++++++++++ modules/adoceanBidAdapter.md | 30 ++++ test/spec/modules/adoceanBidAdapter_spec.js | 158 ++++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 modules/adoceanBidAdapter.js create mode 100644 modules/adoceanBidAdapter.md create mode 100644 test/spec/modules/adoceanBidAdapter_spec.js diff --git a/modules/adoceanBidAdapter.js b/modules/adoceanBidAdapter.js new file mode 100644 index 00000000000..1949e0414a7 --- /dev/null +++ b/modules/adoceanBidAdapter.js @@ -0,0 +1,101 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'adocean'; + +function buildEndpointUrl(emiter, payload) { + let payloadString = ''; + utils._each(payload, function(v, k) { + if (payloadString.length) { + payloadString += '&'; + } + payloadString += k + '=' + encodeURIComponent(v); + }); + + return 'https://' + emiter + '/ad.json?' + payloadString; +} + +function buildRequest(masterBidRequests, masterId) { + const firstBid = masterBidRequests[0]; + const payload = { + id: masterId, + }; + + const bidIdMap = {}; + + utils._each(masterBidRequests, function(v) { + bidIdMap[v.params.slaveId] = v.bidId; + }); + + return { + method: 'GET', + url: buildEndpointUrl(firstBid.params.emiter, payload), + data: {}, + bidIdMap: bidIdMap + }; +} + +function assignToMaster(bidRequest, bidRequestsByMaster) { + const masterId = bidRequest.params.masterId; + bidRequestsByMaster[masterId] = bidRequestsByMaster[masterId] || []; + bidRequestsByMaster[masterId].push(bidRequest); +} + +function interpretResponse(placementResponse, bidRequest, bids) { + if (!placementResponse.error) { + let adCode = '\n\nQuantcast\n\n
\n
', - 'width': 300, - 'height': 250 + statusCode: 1, + placementCode: 'imp1', // Changing this to placementCode to be reflective + cpm: 4.5, + currency: 'USD', + ad: + '
Quantcast
', + creativeId: 1001, + width: 300, + height: 250 } ] }; - beforeEach(() => { - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - addBidReponseStub = sandbox.stub(bidManager, 'addBidResponse'); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - }); + const response = { + body, + headers: {} + }; - afterEach(() => { - sandbox.restore(); - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); + it('should return an empty array if `serverResponse` is `undefined`', () => { + const interpretedResponse = qcSpec.interpretResponse(); - it('should exist and be a function', () => { - expect($$PREBID_GLOBAL$$.handleQuantcastCB).to.exist.and.to.be.a('function'); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when empty text response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(); - sinon.assert.notCalled(addBidReponseStub); - }); + it('should return an empty array if the parsed response does NOT include `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({}); - it('should not add bid when empty json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify({})); - sinon.assert.notCalled(addBidReponseStub); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when malformed json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB('non json text'); - sinon.assert.notCalled(addBidReponseStub); + it('should return an empty array if the parsed response has an empty `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({ bids: [] }); + + expect(interpretedResponse.length).to.equal(0); }); - it('should add a bid object for each bid', () => { - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - sinon.assert.calledOnce(addBidReponseStub); - expect(addBidReponseStub.firstCall.args[0]).to.eql('div-gpt-ad-1438287399331-0'); + it('should get correct bid response', () => { + const expectedResponse = { + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', + cpm: 4.5, + width: 300, + height: 250, + ad: + '
Quantcast
', + ttl: QUANTCAST_TTL, + creativeId: 1001, + netRevenue: QUANTCAST_NET_REVENUE, + currency: 'USD' + }; + const interpretedResponse = qcSpec.interpretResponse(response); + + expect(interpretedResponse[0]).to.deep.equal(expectedResponse); }); - it('should return no bid even when requestId and sizes are missing', () => { - let bidderReponse = { - 'bidderCode': 'quantcast', - 'bids': [ - { - 'statusCode': 0, - 'placementCode': bidderRequest.bids[0].bidId, - } - ] + it('handles no bid response', () => { + const body = { + bidderCode: 'qcx', // Renaming it to use CamelCase since that is what is used in the Prebid.js variable name + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', // Added this field. This is not used now but could be useful in troubleshooting later on. Specially for sites using iFrames + bids: [] + }; + const response = { + body, + headers: {} }; + const expectedResponse = []; + const interpretedResponse = qcSpec.interpretResponse(response); - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - // sinon.assert.calledOnce(addBidReponseStub); - // expect(addBidReponseStub.firstCall.args[0]).to.eql("div-gpt-ad-1438287399331-0"); + expect(interpretedResponse.length).to.equal(0); }); }); }); From 90c66f2ad535e580d78c9ae12c225f9bb1933b35 Mon Sep 17 00:00:00 2001 From: Connor Doherty Date: Thu, 26 Oct 2017 23:36:01 +0300 Subject: [PATCH 15/22] Update yieldmoBid adapter request url (#1771) --- modules/yieldmoBidAdapter.js | 2 +- test/spec/modules/yieldmoBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index d311bb5722c..73f18794ea7 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -19,7 +19,7 @@ var YieldmoAdapter = function YieldmoAdapter() { function buildYieldmoCall(bids) { // build our base tag, based on if we are http or https - var ymURI = '//bid.yieldmo.com/exchange/prebid?'; + var ymURI = '//ads.yieldmo.com/exchange/prebid?'; var ymCall = document.location.protocol + ymURI; // Placement specific information diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 370aeb15457..d2a533be17e 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -52,7 +52,7 @@ describe('Yieldmo adapter', () => { }); it('should load a script with passed bid params', () => { - let route = 'http://bid.yieldmo.com/exchange/prebid?'; + let route = 'http://ads.yieldmo.com/exchange/prebid?'; let requestParams = parseURL(bidRequestURL).search; let parsedPlacementParams = JSON.parse(decodeURIComponent(requestParams.p)); From d5f1cd108a722966aafb6932d94df9f77c41cd7d Mon Sep 17 00:00:00 2001 From: adxcgcom <31470944+adxcgcom@users.noreply.github.com> Date: Thu, 26 Oct 2017 23:08:45 +0200 Subject: [PATCH 16/22] Update adxcg adapter for prebid 1.0 (#1741) * updated adxcg adapter for prebid 1.0 * update for prebid 1.0 with dead code and bidder removed * updates to spec file for prebid 1.0 - removal of biddercode check * updated adxcg bidadapter for prebid-1.0 with response headers --- modules/adxcgBidAdapter.js | 268 ++++++++------- modules/adxcgBidAdapter.md | 50 +++ test/spec/modules/adxcgBidAdapter_spec.js | 390 ++++++++++++---------- 3 files changed, 414 insertions(+), 294 deletions(-) create mode 100644 modules/adxcgBidAdapter.md diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 476cb5989e0..9073a17bda3 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,139 +1,155 @@ -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; -import { STATUS } from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; -import { ajax } from 'src/ajax'; import * as url from 'src/url'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import {NATIVE, VIDEO} from 'src/mediaTypes'; /** - * Adapter for requesting bids from Adxcg - * updated from latest prebid repo on 2017.08.30 + * Adapter for requesting bids from adxcg.net + * updated to latest prebid repo on 2017.10.20 */ -function AdxcgAdapter() { - let bidRequests = {}; - - function _callBids(params) { - if (params.bids && params.bids.length > 0) { - let adZoneIds = []; - let prebidBidIds = []; - let sizes = []; - - params.bids.forEach(bid => { - bidRequests[bid.bidId] = bid; - adZoneIds.push(utils.getBidIdParameter('adzoneid', bid.params)); - prebidBidIds.push(bid.bidId); - sizes.push(utils.parseSizesInput(bid.sizes).join('|')); - }); - - let location = utils.getTopWindowLocation(); - let secure = location.protocol == 'https:'; - - let requestUrl = url.parse(location.href); - requestUrl.search = null; - requestUrl.hash = null; - - let adxcgRequestUrl = url.format({ - protocol: secure ? 'https' : 'http', - hostname: secure ? 'ad-emea-secure.adxcg.net' : 'ad-emea.adxcg.net', - pathname: '/get/adi', - search: { - renderformat: 'javascript', - ver: 'r20141124', - adzoneid: adZoneIds.join(','), - format: sizes.join(','), - prebidBidIds: prebidBidIds.join(','), - url: escape(url.format(requestUrl)), - secure: secure ? '1' : '0' - } - }); - utils.logMessage(`submitting request: ${adxcgRequestUrl}`); - ajax(adxcgRequestUrl, handleResponse, null, { - withCredentials: true - }); - } - } - - function handleResponse(response) { - let adxcgBidReponseList; - - try { - adxcgBidReponseList = JSON.parse(response); - utils.logMessage(`adxcgBidReponseList: ${JSON.stringify(adxcgBidReponseList)}`); - } catch (error) { - adxcgBidReponseList = []; - utils.logError(error); - } +const BIDDER_CODE = 'adxcg'; +const SUPPORTED_AD_TYPES = [VIDEO, NATIVE]; +const SOURCE = 'pbjs10'; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_AD_TYPES, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.adzoneid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * an array of validBidRequests + * Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + utils.logMessage(`buildRequests: ${JSON.stringify(validBidRequests)}`); + + let adZoneIds = []; + let prebidBidIds = []; + let sizes = []; + + validBidRequests.forEach(bid => { + adZoneIds.push(utils.getBidIdParameter('adzoneid', bid.params)); + prebidBidIds.push(bid.bidId); + sizes.push(utils.parseSizesInput(bid.sizes).join('|')); + }); - adxcgBidReponseList.forEach(adxcgBidReponse => { - let bidRequest = bidRequests[adxcgBidReponse.bidId]; - delete bidRequests[adxcgBidReponse.bidId]; - - let bid = bidfactory.createBid(STATUS.GOOD, bidRequest); - - bid.creative_id = adxcgBidReponse.creativeId; - bid.code = 'adxcg'; - bid.bidderCode = 'adxcg'; - bid.cpm = adxcgBidReponse.cpm; - - if (adxcgBidReponse.ad) { - bid.ad = adxcgBidReponse.ad; - } else if (adxcgBidReponse.vastUrl) { - bid.vastUrl = adxcgBidReponse.vastUrl; - bid.descriptionUrl = adxcgBidReponse.vastUrl; - bid.mediaType = 'video'; - } else if (adxcgBidReponse.nativeResponse) { - bid.mediaType = 'native'; - - let nativeResponse = adxcgBidReponse.nativeResponse; - - bid['native'] = { - clickUrl: escape(nativeResponse.link.url), - impressionTrackers: nativeResponse.imptrackers - }; - - nativeResponse.assets.forEach(asset => { - if (asset.title && asset.title.text) { - bid['native'].title = asset.title.text; - } - - if (asset.img && asset.img.url) { - bid['native'].image = asset.img.url; - } - - if (asset.data && asset.data.label == 'DESC' && asset.data.value) { - bid['native'].body = asset.data.value; - } - - if (asset.data && asset.data.label == 'SPONSORED' && asset.data.value) { - bid['native'].sponsoredBy = asset.data.value; - } - }); + let location = utils.getTopWindowLocation(); + let secure = location.protocol === 'https:'; + + let requestUrl = url.parse(location.href); + requestUrl.search = null; + requestUrl.hash = null; + + let adxcgRequestUrl = url.format({ + protocol: secure ? 'https' : 'http', + hostname: secure ? 'hbps.adxcg.net' : 'hbp.adxcg.net', + pathname: '/get/adi', + search: { + renderformat: 'javascript', + ver: 'r20171019PB10', + adzoneid: adZoneIds.join(','), + format: sizes.join(','), + prebidBidIds: prebidBidIds.join(','), + url: encodeURIComponent(url.format(requestUrl)), + secure: secure ? '1' : '0', + source: SOURCE, + pbjs: '$prebid.version$' } + }); - bid.width = adxcgBidReponse.width; - bid.height = adxcgBidReponse.height; + return { + method: 'GET', + url: adxcgRequestUrl, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {bidRequests[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequests) { + let bids = []; + + serverResponse = serverResponse.body; + if (serverResponse) { + serverResponse.forEach(serverResponseOneItem => { + let bid = {}; + + bid.requestId = serverResponseOneItem.bidId; + bid.cpm = serverResponseOneItem.cpm; + bid.creativeId = parseInt(serverResponseOneItem.creativeId); + bid.currency = 'USD'; + bid.netRevenue = serverResponseOneItem.netRevenue ? serverResponseOneItem.netRevenue : true; + bid.ttl = 300; + + if (serverResponseOneItem.deal_id != null && serverResponseOneItem.deal_id.trim().length > 0) { + bid.dealId = serverResponseOneItem.deal_id; + } - utils.logMessage(`submitting bid[${bidRequest.placementCode}]: ${JSON.stringify(bid)}`); - bidmanager.addBidResponse(bidRequest.placementCode, bid); - }); + if (serverResponseOneItem.ad) { + bid.ad = serverResponseOneItem.ad; + } else if (serverResponseOneItem.vastUrl) { + bid.vastUrl = serverResponseOneItem.vastUrl; + bid.descriptionUrl = serverResponseOneItem.vastUrl; + bid.mediaType = 'video'; + } else if (serverResponseOneItem.nativeResponse) { + bid.mediaType = 'native'; + + let nativeResponse = serverResponseOneItem.nativeResponse; + + bid['native'] = { + clickUrl: encodeURIComponent(nativeResponse.link.url), + impressionTrackers: nativeResponse.imptrackers + }; + + nativeResponse.assets.forEach(asset => { + if (asset.title && asset.title.text) { + bid['native'].title = asset.title.text; + } + + if (asset.img && asset.img.url) { + bid['native'].image = asset.img.url; + } + + if (asset.data && asset.data.label === 'DESC' && asset.data.value) { + bid['native'].body = asset.data.value; + } + + if (asset.data && asset.data.label === 'SPONSORED' && asset.data.value) { + bid['native'].sponsoredBy = asset.data.value; + } + }); + } - Object.keys(bidRequests) - .map(bidId => bidRequests[bidId].placementCode) - .forEach(placementCode => { - utils.logMessage(`creating no_bid bid for: ${placementCode}`); - bidmanager.addBidResponse(placementCode, bidfactory.createBid(STATUS.NO_BID)); + bid.width = serverResponseOneItem.width; + bid.height = serverResponseOneItem.height; + utils.logMessage(`submitting bid[${serverResponseOneItem.bidId}]: ${JSON.stringify(bid)}`); + bids.push(bid); }); - }; - - return { - callBids: _callBids - }; + } else { + utils.logMessage(`empty bid response`); + } + return bids; + }, + getUserSyncs: function (syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//cdn.adxcg.net/pb-sync.html' + }]; + } + } }; - -adaptermanager.registerBidAdapter(new AdxcgAdapter(), 'adxcg', { - supportedMediaTypes: ['video', 'native'] -}); - -module.exports = AdxcgAdapter; +registerBidder(spec); diff --git a/modules/adxcgBidAdapter.md b/modules/adxcgBidAdapter.md new file mode 100644 index 00000000000..f3cd8c6d308 --- /dev/null +++ b/modules/adxcgBidAdapter.md @@ -0,0 +1,50 @@ +# Overview + +**Module Name**: Adxcg Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: info@adxcg.com + +# Description + +Module that connects to an Adxcg.com zone to fetch bids. + +# Test Parameters +``` + `` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '1' + } + }] + },{ + code: 'native-ad-div', + sizes: [[300, 250], [1, 1]], + nativeParams: { + title: { required: true, len: 75 }, + image: { required: true }, + body: { len: 200 }, + sponsoredBy: { len: 20 } + }, + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '2379' + } + } + }] + },{ + code: 'video', + sizes: [[640, 480]], + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '20' + } + } + }] + }]; +``` diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index fa55bf92e2e..dbf7359e98d 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,116 +1,62 @@ -import { expect } from 'chai'; -import Adapter from 'modules/adxcgBidAdapter'; -import bidmanager from 'src/bidmanager'; +import {expect} from 'chai'; import * as url from 'src/url'; +import {spec} from 'modules/adxcgBidAdapter'; -const REQUEST = { - 'bidderCode': 'adxcg', - 'bids': [ - { +describe('AdxcgAdapter', () => { + describe('isBidRequestValid', () => { + let bid = { 'bidder': 'adxcg', 'params': { - 'adzoneid': '1', + 'adzoneid': '1' }, - 'sizes': [ - [300, 250], - [640, 360], - [1, 1] - ], + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], 'bidId': '84ab500420319d', - 'bidderRequestId': '7101db09af0db2' - } - ] -}; - -const RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 300, - 'height': 250, - 'creativeId': '42', - 'cpm': 0.45, - 'ad': '' -}] - -const VIDEO_RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 640, - 'height': 360, - 'creativeId': '42', - 'cpm': 0.45, - 'vastUrl': 'vastContentUrl' -}] - -const NATIVE_RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 0, - 'height': 0, - 'creativeId': '42', - 'cpm': 0.45, - 'nativeResponse': { - 'assets': [{ - 'id': 1, - 'required': 0, - 'title': { - 'text': 'titleContent' - } - }, { - 'id': 2, - 'required': 0, - 'img': { - 'url': 'imageContent', - 'w': 600, - 'h': 600 - } - }, { - 'id': 3, - 'required': 0, - 'data': { - 'label': 'DESC', - 'value': 'descriptionContent' - } - }, { - 'id': 0, - 'required': 0, - 'data': { - 'label': 'SPONSORED', - 'value': 'sponsoredByContent' - } - }], - 'link': { - 'url': 'linkContent' - }, - 'imptrackers': ['impressionTracker1', 'impressionTracker2'] - } -}] + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; -describe('AdxcgAdapter', () => { - let adapter; - - beforeEach(() => adapter = new Adapter()); - - describe('request function', () => { - let xhr; - let requests; + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); + }); - afterEach(() => xhr.restore()); + describe('request function http', () => { + let bid = { + 'bidder': 'adxcg', + 'params': { + 'adzoneid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], + 'bidId': '84ab500420319d', + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; it('creates a valid adxcg request url', () => { - adapter.callBids(REQUEST); + let request = spec.buildRequests([bid]); + expect(request).to.exist; + // console.log('IS:' + JSON.stringify(request)); - let parsedRequestUrl = url.parse(requests[0].url); + expect(request.method).to.equal('GET'); + let parsedRequestUrl = url.parse(request.url); - expect(parsedRequestUrl.hostname).to.equal('ad-emea.adxcg.net'); + expect(parsedRequestUrl.hostname).to.equal('hbp.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); let query = parsedRequestUrl.search; expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20141124'); + expect(query.ver).to.equal('r20171019PB10'); + expect(query.source).to.equal('pbjs10'); + expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('1'); expect(query.format).to.equal('300x250|640x360|1x1'); expect(query.jsonp).to.be.empty; @@ -119,94 +65,202 @@ describe('AdxcgAdapter', () => { }); describe('response handler', () => { - let server; + let BIDDER_REQUEST = { + 'bidder': 'adxcg', + 'params': { + 'adzoneid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], + 'bidId': '84ab500420319d', + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); - }); + let BANNER_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 300, + 'height': 250, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'ad': '' + }], + header: {'someheader': 'fakedata'} + } - afterEach(() => { - server.restore() - bidmanager.addBidResponse.restore(); - }); + let BANNER_RESPONSE_WITHDEALID = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 300, + 'height': 250, + 'deal_id': '7722', + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'ad': '' + }], + header: {'someheader': 'fakedata'} + } + + let VIDEO_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 640, + 'height': 360, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'vastUrl': 'vastContentUrl' + }], + header: {'someheader': 'fakedata'} + } + + let NATIVE_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 0, + 'height': 0, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'nativeResponse': { + 'assets': [{ + 'id': 1, + 'required': 0, + 'title': { + 'text': 'titleContent' + } + }, { + 'id': 2, + 'required': 0, + 'img': { + 'url': 'imageContent', + 'w': 600, + 'h': 600 + } + }, { + 'id': 3, + 'required': 0, + 'data': { + 'label': 'DESC', + 'value': 'descriptionContent' + } + }, { + 'id': 0, + 'required': 0, + 'data': { + 'label': 'SPONSORED', + 'value': 'sponsoredByContent' + } + }], + 'link': { + 'url': 'linkContent' + }, + 'imptrackers': ['impressionTracker1', 'impressionTracker2'] + } + }], + header: {'someheader': 'fakedata'} + } it('handles regular responses', () => { - server.respondWith(JSON.stringify(RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(300); - expect(bidResponse.height).to.equal(250); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('banner'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - expect(bidResponse.ad).to.equal(''); + let result = spec.interpretResponse(BANNER_RESPONSE, BIDDER_REQUEST); + + expect(result).to.have.lengthOf(1); + + expect(result[0]).to.exist; + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].ad).to.equal(''); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + expect(result[0].dealId).to.not.exist; + }); + + it('handles regular responses with dealid', () => { + let result = spec.interpretResponse(BANNER_RESPONSE_WITHDEALID, BIDDER_REQUEST); + + expect(result).to.have.lengthOf(1); + + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].ad).to.equal(''); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); }); it('handles video responses', () => { - server.respondWith(JSON.stringify(VIDEO_RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(640); - expect(bidResponse.height).to.equal(360); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('video'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - expect(bidResponse.vastUrl).to.equal('vastContentUrl'); - expect(bidResponse.descriptionUrl).to.equal('vastContentUrl'); + let result = spec.interpretResponse(VIDEO_RESPONSE, BIDDER_REQUEST); + expect(result).to.have.lengthOf(1); + + expect(result[0].width).to.equal(640); + expect(result[0].height).to.equal(360); + expect(result[0].mediaType).to.equal('video'); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].vastUrl).to.equal('vastContentUrl'); + expect(result[0].descriptionUrl).to.equal('vastContentUrl'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); }); it('handles native responses', () => { - server.respondWith(JSON.stringify(NATIVE_RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(0); - expect(bidResponse.height).to.equal(0); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('native'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - - expect(bidResponse.native.clickUrl).to.equal('linkContent'); - expect(bidResponse.native.impressionTrackers).to.deep.equal(['impressionTracker1', 'impressionTracker2']); - expect(bidResponse.native.title).to.equal('titleContent'); - expect(bidResponse.native.image).to.equal('imageContent'); - expect(bidResponse.native.body).to.equal('descriptionContent'); - expect(bidResponse.native.sponsoredBy).to.equal('sponsoredByContent'); + let result = spec.interpretResponse(NATIVE_RESPONSE, BIDDER_REQUEST); + + expect(result[0].width).to.equal(0); + expect(result[0].height).to.equal(0); + expect(result[0].mediaType).to.equal('native'); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + + expect(result[0].native.clickUrl).to.equal('linkContent'); + expect(result[0].native.impressionTrackers).to.deep.equal(['impressionTracker1', 'impressionTracker2']); + expect(result[0].native.title).to.equal('titleContent'); + expect(result[0].native.image).to.equal('imageContent'); + expect(result[0].native.body).to.equal('descriptionContent'); + expect(result[0].native.sponsoredBy).to.equal('sponsoredByContent'); }); it('handles nobid responses', () => { - server.respondWith('[]'); + let response = []; + let bidderRequest = BIDDER_REQUEST; + + let result = spec.interpretResponse(response, bidderRequest); + expect(result.length).to.equal(0); + }); + }); - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + describe('getUserSyncs', () => { + let syncoptionsIframe = { + 'iframeEnabled': 'true' + }; - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.statusMessage).to.equal('Bid returned empty or error response'); + it('should return iframe sync option', () => { + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe'); + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('//cdn.adxcg.net/pb-sync.html'); }); }); }); From c2dd4c771dcb7bdec12e46302dad752f48c1ddd1 Mon Sep 17 00:00:00 2001 From: Matt Kendall Date: Fri, 27 Oct 2017 09:54:44 -0400 Subject: [PATCH 17/22] add vastUrl + media type for video bids Prebid Server (#1739) * add vastUrl + media type for video bids * updates per review * updates per review * updates per review --- modules/prebidServerBidAdapter.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 0906a1a0b3d..89a59fa7c67 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -7,6 +7,7 @@ import { STATUS, S2S } from 'src/constants'; import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; +import { VIDEO } from 'src/mediaTypes'; const getConfig = config.getConfig; @@ -111,7 +112,9 @@ function PrebidServer() { if (videoMediaType) { // pbs expects a ad_unit.video attribute if the imp is video adUnit.video = Object.assign({}, videoMediaType); - delete adUnit.mediaTypes.video; + delete adUnit.mediaTypes; + // default is assumed to be 'banner' so if there is a video type we assume video only until PBS can support multi format auction. + adUnit.media_types = [VIDEO]; } }) convertTypes(adUnits); @@ -196,10 +199,26 @@ function PrebidServer() { bidObject.creative_id = bidObj.creative_id; bidObject.bidderCode = bidObj.bidder; bidObject.cpm = cpm; - bidObject.ad = bidObj.adm; - if (bidObj.nurl) { - bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl)); + // From ORTB see section 4.2.3: adm Optional means of conveying ad markup in case the bid wins; supersedes the win notice if markup is included in both. + if (bidObj.media_type === VIDEO) { + bidObject.mediaType = VIDEO; + if (bidObj.adm) { + bidObject.vastXml = bidObj.adm; + } + if (bidObj.nurl) { + bidObject.vastUrl = bidObj.nurl; + } + } else { + if (bidObj.adm && bidObj.nurl) { + bidObject.ad = bidObj.adm; + bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl)); + } else if (bidObj.adm) { + bidObject.ad = bidObj.adm; + } else if (bidObj.nurl) { + bidObject.adUrl = bidObj.nurl + } } + bidObject.width = bidObj.width; bidObject.height = bidObj.height; bidObject.adserverTargeting = bidObj.ad_server_targeting; @@ -223,7 +242,6 @@ function PrebidServer() { bidObject.source = TYPE; bidObject.adUnitCode = bidRequest.placementCode; bidObject.bidderCode = bidRequest.bidder; - bidmanager.addBidResponse(bidObject.adUnitCode, bidObject); }); }); From a592a7a003ebee8e75342dfb27cf7dc5a0cfee29 Mon Sep 17 00:00:00 2001 From: Niksok Date: Fri, 27 Oct 2017 20:48:42 +0300 Subject: [PATCH 18/22] Fix Centro adapter to allow requests of the same units (#1746) * Add centro adapter and tests for it. * fix bug with different types of bid.sectionID and bid.unit from config * add query parameter adapter=prebid * update tests for centro adapter * fixed bug with call of JSONP callback with name, that contain invalid characters * Centro adapter fix: do not call logError if 'No Bid' was received * Centro adapter: pass the bid request object to bidfactory.createBid * Centro adapter: fix ESLintError * Fix Centro adapter to allow requests of the same units * Fix spec file for Centro adapter --- modules/centroBidAdapter.js | 2 +- test/spec/modules/centroBidAdapter_spec.js | 23 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/modules/centroBidAdapter.js b/modules/centroBidAdapter.js index 9cd3ec004fa..34162f8d1ec 100644 --- a/modules/centroBidAdapter.js +++ b/modules/centroBidAdapter.js @@ -54,7 +54,7 @@ var CentroAdapter = function CentroAdapter() { query.push('sz=' + size.join('x')); } // make handler name for JSONP request - var handlerName = handlerPrefix + bid.unit + size.join('x') + encodeURIComponent(requestedBid.placementCode); + var handlerName = handlerPrefix + bid.unit + size.join('x') + encodeURIComponent(requestedBid.bidId); query.push('callback=' + encodeURIComponent('window["' + handlerName + '"]')); // maybe is needed add some random parameter to disable cache diff --git a/test/spec/modules/centroBidAdapter_spec.js b/test/spec/modules/centroBidAdapter_spec.js index 9f354e1ba56..a4bceb5de39 100644 --- a/test/spec/modules/centroBidAdapter_spec.js +++ b/test/spec/modules/centroBidAdapter_spec.js @@ -43,6 +43,7 @@ describe('centro adapter tests', function () { unit: 28136, page_url: 'http://test_url.ru' }, + bidId: '1234', placementCode: 'div-gpt-ad-12345-1' }, { @@ -51,12 +52,14 @@ describe('centro adapter tests', function () { params: { unit: 28137 }, + bidId: '5678', placementCode: 'div-gpt-ad-12345-2' }, { bidder: 'centro', sizes: [[728, 90]], params: {}, + bidId: '9101112', placementCode: 'div-gpt-ad-12345-3' } ] @@ -71,7 +74,7 @@ describe('centro adapter tests', function () { var parsedBidUrl = urlParse(bidUrl1); var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = 'window["adCentroHandler_28136300x250div-gpt-ad-12345-1"]'; + var generatedCallback = 'window["adCentroHandler_28136300x2501234"]'; expect(parsedBidUrl.hostname).to.equal('staging.brand-server.com'); expect(parsedBidUrl.pathname).to.equal('/hb'); @@ -85,7 +88,7 @@ describe('centro adapter tests', function () { parsedBidUrl = urlParse(bidUrl2); parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - generatedCallback = 'window["adCentroHandler_28137728x90div-gpt-ad-12345-2"]'; + generatedCallback = 'window["adCentroHandler_28137728x905678"]'; expect(parsedBidUrl.hostname).to.equal('t.brand-server.com'); expect(parsedBidUrl.pathname).to.equal('/hb'); @@ -117,6 +120,7 @@ describe('centro adapter tests', function () { params: { unit: 28136 }, + bidId: '12345', placementCode: '/19968336/header-bid-tag-0' }, { @@ -125,6 +129,7 @@ describe('centro adapter tests', function () { params: { unit: 111111 }, + bidId: '12346', placementCode: '/19968336/header-bid-tag-1' }, { @@ -133,6 +138,7 @@ describe('centro adapter tests', function () { params: { unit: 222222 }, + bidId: '12347', placementCode: '/19968336/header-bid-tag-2' }, { @@ -141,6 +147,7 @@ describe('centro adapter tests', function () { params: { unit: 333333 }, + bidId: '12348', placementCode: '/19968336/header-bid-tag-3' } ] @@ -149,9 +156,9 @@ describe('centro adapter tests', function () { it('callback function should exist', function () { adapter().callBids(params); - expect(window['adCentroHandler_28136300x250%2F19968336%2Fheader-bid-tag-0']) + expect(window['adCentroHandler_28136300x25012345']) .to.exist.and.to.be.a('function'); - expect(window['adCentroHandler_111111728x90%2F19968336%2Fheader-bid-tag-1']) + expect(window['adCentroHandler_111111728x9012346']) .to.exist.and.to.be.a('function'); }); @@ -180,10 +187,10 @@ describe('centro adapter tests', function () { var response3 = {'adTag': '', 'height': 0, 'value': 0, 'width': 0, 'sectionID': 222222}; var response4 = ''; - window['adCentroHandler_28136300x250%2F19968336%2Fheader-bid-tag-0'](response); - window['adCentroHandler_111111728x90%2F19968336%2Fheader-bid-tag-1'](response2); - window['adCentroHandler_222222728x90%2F19968336%2Fheader-bid-tag-2'](response3); - window['adCentroHandler_333333728x90%2F19968336%2Fheader-bid-tag-3'](response4); + window['adCentroHandler_28136300x25012345'](response); + window['adCentroHandler_111111728x9012346'](response2); + window['adCentroHandler_222222728x9012347'](response3); + window['adCentroHandler_333333728x9012348'](response4); var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; var bidObject1 = stubAddBidResponse.getCall(0).args[1]; From 265a8a9b6fbcff24fbc7caabbb2df6113efc100d Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Mon, 30 Oct 2017 06:38:20 -0600 Subject: [PATCH 19/22] Change prebidServer to call client user syncs if they exist (#1734) --- modules/prebidServerBidAdapter.js | 8 +++++ modules/rubiconBidAdapter.js | 4 +-- src/adaptermanager.js | 4 +++ src/adapters/bidderFactory.js | 33 +++++++++++-------- .../modules/prebidServerBidAdapter_spec.js | 18 ++++++++++ test/spec/modules/rubiconBidAdapter_spec.js | 9 +++-- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 89a59fa7c67..7120d67eb56 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -183,6 +183,14 @@ function PrebidServer() { }); } + // do client-side syncs if available + requestedBidders.forEach(bidder => { + let clientAdapter = adaptermanager.getBidAdapter(bidder); + if (clientAdapter && clientAdapter.registerSyncs) { + clientAdapter.registerSyncs(); + } + }); + if (result.bids) { result.bids.forEach(bidObj => { let bidRequest = utils.getBidRequest(bidObj.bid_id); diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 866e02bc258..2830711a4c9 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -287,8 +287,8 @@ export const spec = { return bids; }, []); }, - getUserSyncs: function() { - if (!hasSynced) { + getUserSyncs: function(syncOptions) { + if (!hasSynced && syncOptions.iframeEnabled) { hasSynced = true; return { type: 'iframe', diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 37ab5dffafb..34866365445 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -325,6 +325,10 @@ exports.setBidderSequence = function (order) { } }; +exports.getBidAdapter = function(bidder) { + return _bidderRegistry[bidder]; +}; + exports.setS2SConfig = function (config) { _s2sConfig = config; }; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 241891640c5..9641c4c484b 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -154,6 +154,7 @@ export function newBidder(spec) { getSpec: function() { return Object.freeze(spec); }, + registerSyncs, callBids: function(bidderRequest) { if (!Array.isArray(bidderRequest.bids)) { return; @@ -195,20 +196,7 @@ export function newBidder(spec) { const responses = []; function afterAllResponses() { fillNoBids(); - if (spec.getUserSyncs) { - let syncs = spec.getUserSyncs({ - iframeEnabled: config.getConfig('userSync.iframeEnabled'), - pixelEnabled: config.getConfig('userSync.pixelEnabled'), - }, responses); - if (syncs) { - if (!Array.isArray(syncs)) { - syncs = [syncs]; - } - syncs.forEach((sync) => { - userSync.registerSync(sync.type, spec.code, sync.url) - }); - } - } + registerSyncs(responses); } const validBidRequests = bidderRequest.bids.filter(filterAndWarn); @@ -337,6 +325,23 @@ export function newBidder(spec) { } }); + function registerSyncs(responses) { + if (spec.getUserSyncs) { + let syncs = spec.getUserSyncs({ + iframeEnabled: config.getConfig('userSync.iframeEnabled'), + pixelEnabled: config.getConfig('userSync.pixelEnabled'), + }, responses); + if (syncs) { + if (!Array.isArray(syncs)) { + syncs = [syncs]; + } + syncs.forEach((sync) => { + userSync.registerSync(sync.type, spec.code, sync.url) + }); + } + } + } + function filterAndWarn(bid) { if (!spec.isBidRequestValid(bid)) { logWarn(`Invalid bid sent to bidder ${spec.code}: ${JSON.stringify(bid)}`); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 21098a2859f..78eef4b016b 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import Adapter from 'modules/prebidServerBidAdapter'; +import adapterManager from 'src/adaptermanager'; import bidmanager from 'src/bidmanager'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; @@ -353,6 +354,23 @@ describe('S2S Adapter', () => { expect(response).to.have.property('adserverTargeting').that.deep.equals({'foo': 'bar'}); }); + it('registers client user syncs when client bid adapter is present', () => { + let rubiconAdapter = { + registerSyncs: sinon.spy() + }; + sinon.stub(adapterManager, 'getBidAdapter', () => rubiconAdapter); + + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + adapter.setConfig(CONFIG); + adapter.callBids(REQUEST); + server.respond(); + + sinon.assert.calledOnce(rubiconAdapter.registerSyncs); + + adapterManager.getBidAdapter.restore(); + }); + it('registers bid responses when server requests cookie sync', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 6c08c66f485..f77391dffe2 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -4,6 +4,7 @@ import { spec, masSizeOrdering, resetUserSync } from 'modules/rubiconBidAdapter' import { parse as parseQuery } from 'querystring'; import { newBidder } from 'src/adapters/bidderFactory'; import { userSync } from 'src/userSync'; +import { config } from 'src/config'; var CONSTANTS = require('src/constants.json'); @@ -779,13 +780,17 @@ describe('the rubicon adapter', () => { }); it('should register the Emily iframe', () => { - let syncs = spec.getUserSyncs(); + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); }); it('should not register the Emily iframe more than once', () => { - let syncs = spec.getUserSyncs(); + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); // when called again, should still have only been called once From 8420558d2e565b1951713b9a90a940bfb9d0b248 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Mon, 30 Oct 2017 08:53:18 -0600 Subject: [PATCH 20/22] New hooks API (replaces monkey-patching for currency) (#1683) * added hook module to prebid core that allows extension of arbitrary functions * remove unused dependency tiny-queue * change PluginFunction to HookedFunction * more hook documentation fixes * allow context for hooked functions * added tests for context * remove withContext, just use bind * fix in hooks so asyncSeries keeps proper bound context --- modules/currency.js | 67 ++++++------- src/bidmanager.js | 5 +- src/hook.js | 78 +++++++++++++++ test/spec/hook_spec.js | 151 +++++++++++++++++++++++++++++ test/spec/modules/currency_spec.js | 60 +++++------- 5 files changed, 283 insertions(+), 78 deletions(-) create mode 100644 src/hook.js create mode 100644 test/spec/hook_spec.js diff --git a/modules/currency.js b/modules/currency.js index 1d0286ed569..19e7d2903f4 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -13,9 +13,6 @@ var conversionCache = {}; var currencyRatesLoaded = false; var adServerCurrency = 'USD'; -// Used as reference to the original bidmanager.addBidResponse -var originalBidResponse; - export var currencySupportEnabled = false; export var currencyRates = {}; var bidderCurrencyDefault = {}; @@ -81,12 +78,9 @@ function initCurrency(url) { conversionCache = {}; currencySupportEnabled = true; - if (!originalBidResponse) { - utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + utils.logInfo('Installing addBidResponse decorator for currency module', arguments); - originalBidResponse = bidmanager.addBidResponse; - bidmanager.addBidResponse = addBidResponseDecorator(bidmanager.addBidResponse); - } + bidmanager.addBidResponse.addHook(addBidResponseHook, 100); if (!currencyRates.conversions) { ajax(url, function (response) { @@ -103,12 +97,9 @@ function initCurrency(url) { } function resetCurrency() { - if (originalBidResponse) { - utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); + utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); - bidmanager.addBidResponse = originalBidResponse; - originalBidResponse = undefined; - } + bidmanager.addBidResponse.removeHook(addBidResponseHook); adServerCurrency = 'USD'; conversionCache = {}; @@ -118,37 +109,35 @@ function resetCurrency() { bidderCurrencyDefault = {}; } -export function addBidResponseDecorator(fn) { - return function(adUnitCode, bid) { - if (!bid) { - return fn.apply(this, arguments); // if no bid, call original and let it display warnings - } +export function addBidResponseHook(adUnitCode, bid, fn) { + if (!bid) { + return fn.apply(this, arguments); // if no bid, call original and let it display warnings + } - let bidder = bid.bidderCode || bid.bidder; - if (bidderCurrencyDefault[bidder]) { - let currencyDefault = bidderCurrencyDefault[bidder]; - if (bid.currency && currencyDefault !== bid.currency) { - utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); - } else { - bid.currency = currencyDefault; - } + let bidder = bid.bidderCode || bid.bidder; + if (bidderCurrencyDefault[bidder]) { + let currencyDefault = bidderCurrencyDefault[bidder]; + if (bid.currency && currencyDefault !== bid.currency) { + utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); + } else { + bid.currency = currencyDefault; } + } - // default to USD if currency not set - if (!bid.currency) { - utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); - bid.currency = 'USD'; - } + // default to USD if currency not set + if (!bid.currency) { + utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); + bid.currency = 'USD'; + } - // execute immediately if the bid is already in the desired currency - if (bid.currency === adServerCurrency) { - return fn.apply(this, arguments); - } + // execute immediately if the bid is already in the desired currency + if (bid.currency === adServerCurrency) { + return fn.apply(this, arguments); + } - bidResponseQueue.push(wrapFunction(fn, this, arguments)); - if (!currencySupportEnabled || currencyRatesLoaded) { - processBidResponseQueue(); - } + bidResponseQueue.push(wrapFunction(fn, this, arguments)); + if (!currencySupportEnabled || currencyRatesLoaded) { + processBidResponseQueue(); } } diff --git a/src/bidmanager.js b/src/bidmanager.js index c12cc4828e6..abd8fea135d 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -5,6 +5,7 @@ import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; import { config } from 'src/config'; +import { createHook } from 'src/hook'; var CONSTANTS = require('./constants.json'); var AUCTION_END = CONSTANTS.EVENTS.AUCTION_END; @@ -82,7 +83,7 @@ exports.bidsBackAll = function () { /* * This function should be called to by the bidder adapter to register a bid response */ -exports.addBidResponse = function (adUnitCode, bid) { +exports.addBidResponse = createHook('asyncSeries', function (adUnitCode, bid) { if (isValid()) { prepareBidForAuction(); @@ -250,7 +251,7 @@ exports.addBidResponse = function (adUnitCode, bid) { doCallbacksIfNeeded(); } } -}; +}); function getKeyValueTargetingPairs(bidderCode, custBidObj) { var keyValues = {}; diff --git a/src/hook.js b/src/hook.js new file mode 100644 index 00000000000..5ba1d4b9bbf --- /dev/null +++ b/src/hook.js @@ -0,0 +1,78 @@ + +/** + * @typedef {function} HookedFunction + * @property {function(function(), [number])} addHook A method that takes a new function to attach as a hook + * to the HookedFunction + * @property {function(function())} removeHook A method to remove attached hooks + */ + +/** + * A map of global hook methods to allow easy extension of hooked functions that are intended to be extended globally + * @type {{}} + */ +export const hooks = {}; + +/** + * A utility function for allowing a regular function to be extensible with additional hook functions + * @param {string} type The method for applying all attached hooks when this hooked function is called + * @param {function()} fn The function to make hookable + * @param {string} hookName If provided this allows you to register a name for a global hook to have easy access to + * the addHook and removeHook methods for that hook (which are usually accessed as methods on the function itself) + * @returns {HookedFunction} A new function that implements the HookedFunction interface + */ +export function createHook(type, fn, hookName) { + let _hooks = [{fn, priority: 0}]; + + let types = { + sync: function(...args) { + _hooks.forEach(hook => { + hook.fn.apply(this, args); + }); + }, + asyncSeries: function(...args) { + let curr = 0; + + const asyncSeriesNext = (...args) => { + let hook = _hooks[++curr]; + if (typeof hook === 'object' && typeof hook.fn === 'function') { + return hook.fn.apply(this, args.concat(asyncSeriesNext)) + } + }; + + return _hooks[curr].fn.apply(this, args.concat(asyncSeriesNext)); + } + }; + + if (!types[type]) { + throw 'invalid hook type'; + } + + let methods = { + addHook: function(fn, priority = 10) { + if (typeof fn === 'function') { + _hooks.push({ + fn, + priority: priority + }); + + _hooks.sort((a, b) => b.priority - a.priority); + } + }, + removeHook: function(removeFn) { + _hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); + } + }; + + if (typeof hookName === 'string') { + hooks[hookName] = methods; + } + + function hookedFn(...args) { + if (_hooks.length === 0) { + return fn.apply(this, args); + } + return types[type].apply(this, args); + } + + return Object.assign(hookedFn, methods); +} diff --git a/test/spec/hook_spec.js b/test/spec/hook_spec.js new file mode 100644 index 00000000000..1fab4ecd1b7 --- /dev/null +++ b/test/spec/hook_spec.js @@ -0,0 +1,151 @@ + +import { expect } from 'chai'; +import { createHook, hooks } from 'src/hook'; + +describe('the hook module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should call all sync hooks attached to a function', () => { + let called = []; + let calledWith; + + let testFn = () => { + called.push(testFn); + }; + let testHook = (...args) => { + called.push(testHook); + calledWith = args; + }; + let testHook2 = () => { + called.push(testHook2); + }; + let testHook3 = () => { + called.push(testHook3); + }; + + let hookedTestFn = createHook('sync', testFn, 'testHook'); + + hookedTestFn.addHook(testHook, 50); + hookedTestFn.addHook(testHook2, 100); + + // make sure global test hooks work as well (with default priority) + hooks['testHook'].addHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testHook, + testHook3, + testFn + ]); + + expect(calledWith).to.deep.equal([1, 2, 3]); + + called = []; + + hookedTestFn.removeHook(testHook); + hooks['testHook'].removeHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext = {}; + let calledBoundContext; + let hook = function() { + calledBoundContext = this; + }.bind(boundContext); + + let hookFn = createHook('sync', fn); + hookFn.addHook(hook); + + let newContext = {}; + hookFn.bind(newContext)(); + + expect(context).to.equal(newContext); + expect(calledBoundContext).to.equal(boundContext); + }); + + describe('asyncSeries', () => { + it('should call function as normal if no hooks attached', () => { + let fn = sandbox.spy(); + let hookFn = createHook('asyncSeries', fn); + + hookFn(1); + + expect(fn.calledOnce).to.equal(true); + expect(fn.firstCall.args[0]).to.equal(1); + }); + + it('should call hooks correctly applied in asyncSeries', () => { + let called = []; + + let testFn = (called) => { + called.push(testFn); + }; + let testHook = (called, next) => { + called.push(testHook); + next(called); + }; + let testHook2 = (called, next) => { + called.push(testHook2); + next(called); + }; + + let hookedTestFn = createHook('asyncSeries', testFn); + hookedTestFn.addHook(testHook); + hookedTestFn.addHook(testHook2); + + hookedTestFn(called); + + expect(called).to.deep.equal([ + testHook, + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext1 = {}; + let calledBoundContext1; + let hook1 = function(next) { + calledBoundContext1 = this; + next() + }.bind(boundContext1); + + let hookFn = createHook('asyncSeries', fn); + hookFn.addHook(hook1); + + let newContext = {}; + hookFn = hookFn.bind(newContext); + hookFn(); + + expect(context).to.equal(newContext); + expect(calledBoundContext1).to.equal(boundContext1); + }); + }); +}); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 937e6a084e4..06faa5665c9 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -5,7 +5,7 @@ import { import { setConfig, - addBidResponseDecorator, + addBidResponseHook, currencySupportEnabled, currencyRates @@ -46,10 +46,6 @@ describe('currency', function () { var bid = { cpm: 1, bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'GBP', bidderCurrencyDefault: { @@ -57,7 +53,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('GBP') }); @@ -68,10 +66,6 @@ describe('currency', function () { var bid = { cpm: 1, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'JPY', bidderCurrencyDefault: { @@ -79,7 +73,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('JPY') }); @@ -97,12 +93,10 @@ describe('currency', function () { var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); - expect(innerBid.cpm).to.equal('1.0000'); }); }); @@ -113,14 +107,15 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var marker = false; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - marker = true; - }); var bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - wrappedAddBidResponseFn('elementId', bid); + + var marker = false; + addBidResponseHook('elementId', bid, function() { + marker = true; + }); + expect(marker).to.equal(false); fakeCurrencyFileServer.respond(); @@ -133,10 +128,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); }); @@ -144,10 +138,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -157,10 +150,9 @@ describe('currency', function () { }); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(bid).to.equal(innerBid); }); @@ -170,10 +162,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'ABC' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -183,10 +174,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -196,10 +186,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); expect(innerBid.currency).to.equal('JPY'); }); @@ -210,10 +199,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.7798'); expect(innerBid.currency).to.equal('GBP'); }); @@ -224,10 +212,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'CNY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.1133'); expect(innerBid.currency).to.equal('GBP'); }); @@ -238,10 +225,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.0623'); expect(innerBid.currency).to.equal('CNY'); }); From 47019481bb34f4071f85d899a16727d8aae606e0 Mon Sep 17 00:00:00 2001 From: Matt Kendall Date: Mon, 30 Oct 2017 10:55:19 -0400 Subject: [PATCH 21/22] Add `usePaymentRule` param to AN bidders (#1778) --- modules/appnexusAstBidAdapter.js | 1 + modules/appnexusBidAdapter.js | 4 ++++ test/spec/modules/appnexusAstBidAdapter_spec.js | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 3c900e15312..f3173dc15bc 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -248,6 +248,7 @@ function bidToTag(bid) { tag.code = bid.params.invCode; } tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false tag.prebid = true; tag.disable_psa = true; if (bid.params.reserve) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 3d6549542dc..1fb48b68fc4 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -44,12 +44,15 @@ AppNexusAdapter = function AppNexusAdapter() { var query = utils.getBidIdParameter('query', bid.params); var referrer = utils.getBidIdParameter('referrer', bid.params); var altReferrer = utils.getBidIdParameter('alt_referrer', bid.params); + let usePaymentRule = utils.getBidIdParameter('usePaymentRule', bid.params); var jptCall = '//ib.adnxs.com/jpt?'; jptCall = utils.tryAppendQueryString(jptCall, 'callback', '$$PREBID_GLOBAL$$.handleAnCB'); jptCall = utils.tryAppendQueryString(jptCall, 'callback_uid', callbackId); jptCall = utils.tryAppendQueryString(jptCall, 'psa', '0'); jptCall = utils.tryAppendQueryString(jptCall, 'id', placementId); + jptCall = utils.tryAppendQueryString(jptCall, 'use_pmt_rule', usePaymentRule); + if (member) { jptCall = utils.tryAppendQueryString(jptCall, 'member', member); } else if (memberId) { @@ -106,6 +109,7 @@ AppNexusAdapter = function AppNexusAdapter() { delete paramsCopy.referrer; delete paramsCopy.alt_referrer; delete paramsCopy.member; + delete paramsCopy.usePaymentRule; // get the reminder var queryParams = utils.parseQueryStringParameters(paramsCopy); diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index 83cbcc38a2b..3884b1c5863 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -244,6 +244,23 @@ describe('AppNexusAdapter', () => { 'value': ['123'] }]); }); + + it('should should add payment rules to the request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); }) describe('interpretResponse', () => { From ac6775b72463004e34292bd82108541cfb28ac6a Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 30 Oct 2017 21:56:38 +0300 Subject: [PATCH 22/22] Update GetIntent adapter to 1.0 version (#1721) * AD-2311: Make GetIntent adapter compatible with Prebid.js 1.0 version * AD-2311: remove blank line * Trigger * GetIntent adapter - added bid response fields: currency, ttl and netRevenue; fixed creative size selector (#1721) * GetIntent adapter - added bid response fields: bidId, creativeId (#1721) --- modules/getintentBidAdapter.js | 191 +++++++++----- modules/getintentBidAdapter.md | 48 ++++ test/spec/modules/getintentBidAdapter_spec.js | 249 ++++++++---------- 3 files changed, 293 insertions(+), 195 deletions(-) create mode 100644 modules/getintentBidAdapter.md diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 72f1c1a0073..f677b107529 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,78 +1,143 @@ -import { STATUS } from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; +import { registerBidder } from 'src/adapters/bidderFactory'; -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader.js'); +const BIDDER_CODE = 'getintent'; +const IS_NET_REVENUE = true; +const BID_HOST = 'px.adhigh.net'; +const BID_BANNER_PATH = '/rtb/direct_banner'; +const BID_VIDEO_PATH = '/rtb/direct_vast'; +const BID_RESPONSE_TTL_SEC = 360; +const VIDEO_PROPERTIES = [ + 'protocols', 'mimes', 'min_dur', 'max_dur', 'min_btr', 'max_btr', 'vi_format', 'api', 'skippable' +]; +const OPTIONAL_PROPERTIES = [ + 'cur', 'floor' +]; -var GetIntentAdapter = function GetIntentAdapter() { - var headerBiddingStaticJS = window.location.protocol + '//cdn.adhigh.net/adserver/hb.js'; +export const spec = { + code: BIDDER_CODE, + aliases: ['getintentAdapter'], + supportedMediaTypes: ['video', 'banner'], - function _callBids(params) { - if (typeof window.gi_hb === 'undefined') { - adloader.loadScript(headerBiddingStaticJS, function() { - bid(params); - }, true); - } else { - bid(params); + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid to validate. + * @return {boolean} True if this is a valid bid, and false otherwise. + * */ + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.pid && bid.params.tid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests - an array of bids. + * @return ServerRequest[] + */ + buildRequests: function(bidRequests) { + return bidRequests.map(bidRequest => { + let giBidRequest = buildGiBidRequest(bidRequest); + return { + method: 'GET', + url: buildUrl(giBidRequest), + data: giBidRequest, + }; + }); + }, + + /** + * Callback for bids, after the call to DSP completes. + * Parse the response from the server into a list of bids. + * + * @param {object} serverResponse A response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse) { + let responseBody = serverResponse.body; + const bids = []; + if (responseBody && responseBody.no_bid !== 1) { + let size = parseSize(responseBody.size); + let bid = { + requestId: responseBody.bid_id, + ttl: BID_RESPONSE_TTL_SEC, + netRevenue: IS_NET_REVENUE, + currency: responseBody.currency, + creativeId: responseBody.creative_id, + cpm: responseBody.cpm, + width: size[0], + height: size[1] + }; + if (responseBody.vast_url) { + bid.mediaType = 'video'; + bid.vastUrl = responseBody.vast_url; + } else { + bid.mediaType = 'banner'; + bid.ad = responseBody.ad; + } + bids.push(bid); } + return bids; } - function addOptional(params, request, props) { - for (var i = 0; i < props.length; i++) { - if (params.hasOwnProperty(props[i])) { - request[props[i]] = params[props[i]]; +} + +function buildUrl(bid) { + return '//' + BID_HOST + (bid.is_video ? BID_VIDEO_PATH : BID_BANNER_PATH); +} + +/** + * Builds GI bid request from BidRequest. + * + * @param {BidRequest} bidRequest. + * @return {object} GI bid request. + * */ +function buildGiBidRequest(bidRequest) { + let giBidRequest = { + bid_id: bidRequest.bidId, + pid: bidRequest.params.pid, // required + tid: bidRequest.params.tid, // required + known: bidRequest.params.known || 1, + is_video: bidRequest.mediaType === 'video', + resp_type: 'JSON' + }; + if (bidRequest.sizes) { + giBidRequest.size = produceSize(bidRequest.sizes); + } + addVideo(bidRequest.params.video, giBidRequest); + addOptional(bidRequest.params, giBidRequest, OPTIONAL_PROPERTIES); + return giBidRequest; +} + +function addVideo(video, giBidRequest) { + if (giBidRequest.is_video && video) { + for (let i = 0, l = VIDEO_PROPERTIES.length; i < l; i++) { + let key = VIDEO_PROPERTIES[i]; + if (video.hasOwnProperty(key)) { + giBidRequest[key] = Array.isArray(video[key]) ? video[key].join(',') : video[key]; } } } +} - function bid(params) { - var bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - var bidRequest = bids[i]; - var request = { - pid: bidRequest.params.pid, // required - tid: bidRequest.params.tid, // required - known: bidRequest.params.known || 1, - is_video: bidRequest.mediaType === 'video', - video: bidRequest.params.video || {}, - size: bidRequest.sizes[0].join('x'), - }; - addOptional(bidRequest.params, request, ['cur', 'floor']); - (function (r, br) { - window.gi_hb.makeBid(r, function(bidResponse) { - if (bidResponse.no_bid === 1) { - var nobid = bidfactory.createBid(STATUS.NO_BID); - nobid.bidderCode = br.bidder; - bidmanager.addBidResponse(br.placementCode, nobid); - } else { - var bid = bidfactory.createBid(STATUS.GOOD); - var size = bidResponse.size.split('x'); - bid.bidderCode = br.bidder; - bid.cpm = bidResponse.cpm; - bid.width = size[0]; - bid.height = size[1]; - if (br.mediaType === 'video') { - bid.vastUrl = bidResponse.vast_url; - bid.descriptionUrl = bidResponse.vast_url; - bid.mediaType = 'video'; - } else { - bid.ad = bidResponse.ad; - } - bidmanager.addBidResponse(br.placementCode, bid); - } - }); - })(request, bidRequest); +function addOptional(params, request, props) { + for (let i = 0; i < props.length; i++) { + if (params.hasOwnProperty(props[i])) { + request[props[i]] = params[props[i]]; } } +} - return { - callBids: _callBids - }; -}; +function parseSize(s) { + return s.split('x').map(Number); +} -adaptermanager.registerBidAdapter(new GetIntentAdapter(), 'getintent', { - supportedMediaTypes: ['video'] -}); +function produceSize(sizes) { + // TODO: add support for multiple sizes + if (Array.isArray(sizes[0])) { + return sizes[0].join('x'); + } else { + return sizes.join('x'); + } +} -module.exports = GetIntentAdapter; +registerBidder(spec); diff --git a/modules/getintentBidAdapter.md b/modules/getintentBidAdapter.md new file mode 100644 index 00000000000..7f9b38f6b22 --- /dev/null +++ b/modules/getintentBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +``` +Module Name: GetIntent Bidder Adapter +Module Type: Bidder Adapter +Maintainer: server-dev@getintent.com +``` + +# Description + +Module that connects to GetIntent's demand sources. +Banner and Video formats are supported. + +# Required parameters +* ```pid``` for Publisher ID +* ```tid``` for Tag ID. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: "getintent", + params: { + pid: "7", + tid: "test01" + } + } + ] + },{ + code: 'test-video-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: "getintent", + params: { + pid: "7", + tid: "test01" + }, + mediaType: "video" + } + ] + } + ]; +``` diff --git a/test/spec/modules/getintentBidAdapter_spec.js b/test/spec/modules/getintentBidAdapter_spec.js index e66d2138eaf..1b76c4852b4 100644 --- a/test/spec/modules/getintentBidAdapter_spec.js +++ b/test/spec/modules/getintentBidAdapter_spec.js @@ -1,146 +1,131 @@ -import Adapter from '../../../modules/getintentBidAdapter'; -import bidManager from '../../../src/bidmanager'; -import {expect} from 'chai'; - -var assert = require('chai').assert; - -describe('getintent media adapter test', () => { - let adapter; - - window.gi_hb = { - makeBid: function(bidRequest, callback) { - var pid = bidRequest.pid; - var tid = bidRequest.tid; - - if (pid == 'p1' || pid == 'p2') { - callback({ - ad: `Ad Markup ${pid} ${tid}`, - cpm: 2.71, - size: `${bidRequest.size}` - }, bidRequest); - } else if (pid == 'p3') { - callback({ - no_bid: 1 - }, bidRequest); - } else if (pid == 'p4') { - callback({ - vast_url: `http://test.com?pid=${pid}&tid=${tid}`, - cpm: 2.88, - size: `${bidRequest.size}` - }, bidRequest); +import { expect } from 'chai' +import { spec } from 'modules/getintentBidAdapter' + +describe('GetIntent Adapter Tests:', () => { + const bidRequests = [{ + bidId: 'bid12345', + params: { + pid: 'p1000', + tid: 't1000' + }, + sizes: [[300, 250]] + }]; + const videoBidRequest = { + bidId: 'bid789', + params: { + pid: 'p1001', + tid: 't1001', + video: { + mimes: ['video/mp4', 'application/javascript'], + max_dur: 20, + api: [1, 2], + skippable: true } - } + }, + sizes: [300, 250], + mediaType: 'video' }; - function callOut() { - adapter.callBids({ - bidderCode: 'getintent', - bids: [ - { - bidder: 'getintent', - adUnitCode: 'test1', - sizes: [[320, 240]], - params: { - pid: 'p1', - tid: 't1', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test2', - sizes: [[720, 90]], - params: { - pid: 'p2', - tid: 't1', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test3', - sizes: [[400, 500]], - params: { - pid: 'p3', - tid: 't2', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test4', - mediaType: 'video', - sizes: [[480, 352]], - params: { - pid: 'p4', - tid: 't3', - cur: 'USD' - } - } - ] - }); - } - - beforeEach(() => { - adapter = new Adapter(); + it('Verify build request', () => { + const serverRequests = spec.buildRequests(bidRequests); + let serverRequest = serverRequests[0]; + expect(serverRequest.url).to.equal('//px.adhigh.net/rtb/direct_banner'); + expect(serverRequest.method).to.equal('GET'); + expect(serverRequest.data.bid_id).to.equal('bid12345'); + expect(serverRequest.data.pid).to.equal('p1000'); + expect(serverRequest.data.tid).to.equal('t1000'); + expect(serverRequest.data.size).to.equal('300x250'); + expect(serverRequest.data.is_video).to.equal(false); }); - afterEach(() => { + it('Verify build video request', () => { + const serverRequests = spec.buildRequests([videoBidRequest]); + let serverRequest = serverRequests[0]; + expect(serverRequest.url).to.equal('//px.adhigh.net/rtb/direct_vast'); + expect(serverRequest.method).to.equal('GET'); + expect(serverRequest.data.bid_id).to.equal('bid789'); + expect(serverRequest.data.pid).to.equal('p1001'); + expect(serverRequest.data.tid).to.equal('t1001'); + expect(serverRequest.data.size).to.equal('300x250'); + expect(serverRequest.data.is_video).to.equal(true); + expect(serverRequest.data.mimes).to.equal('video/mp4,application/javascript'); + expect(serverRequest.data.max_dur).to.equal(20); + expect(serverRequest.data.api).to.equal('1,2'); + expect(serverRequest.data.skippable).to.equal(true); }); - describe('adding bids to the manager', () => { - let firstBid; - let secondBid; - let thirdBid; - let videoBid; - - beforeEach(() => { - sinon.stub(bidManager, 'addBidResponse'); - callOut(); - firstBid = bidManager.addBidResponse.firstCall.args[1]; - secondBid = bidManager.addBidResponse.secondCall.args[1]; - thirdBid = bidManager.addBidResponse.thirdCall.args[1]; - videoBid = bidManager.addBidResponse.lastCall.args[1]; - }); - - afterEach(() => { - bidManager.addBidResponse.restore(); - }); - - it('was called four times', () => { - assert.strictEqual(bidManager.addBidResponse.callCount, 4); - }); + it('Verify parse response', () => { + const serverResponse = { + body: { + bid_id: 'bid12345', + cpm: 2.25, + currency: 'USD', + size: '300x250', + creative_id: '1000', + ad: 'Ad markup' + }, + headers: { + } + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(2.25); + expect(bid.currency).to.equal('USD'); + expect(bid.creativeId).to.equal('1000'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.ad).to.equal('Ad markup'); + }); - it('will respond to the first bid', () => { - expect(firstBid).to.have.property('ad', 'Ad Markup p1 t1'); - expect(firstBid).to.have.property('cpm', 2.71); - expect(firstBid).to.have.property('width', '320'); - expect(firstBid).to.have.property('height', '240'); - }); + it('Verify parse video response', () => { + const serverResponse = { + body: { + bid_id: 'bid789', + cpm: 3.25, + currency: 'USD', + size: '300x250', + creative_id: '2000', + vast_url: '//vast.xml/url' + }, + headers: { + } + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(3.25); + expect(bid.currency).to.equal('USD'); + expect(bid.creativeId).to.equal('2000'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.requestId).to.equal('bid789'); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('//vast.xml/url'); + }); - it('will respond to the second bid', () => { - expect(secondBid).to.have.property('ad', 'Ad Markup p2 t1'); - expect(secondBid).to.have.property('cpm', 2.71); - expect(secondBid).to.have.property('width', '720'); - expect(secondBid).to.have.property('height', '90'); - }); + it('Verify bidder code', () => { + expect(spec.code).to.equal('getintent'); + }); - it('wont respond to the third bid', () => { - expect(thirdBid).to.not.have.property('ad'); - expect(thirdBid).to.not.have.property('cpm'); - }); + it('Verify bidder aliases', () => { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('getintentAdapter'); + }); - it('will add the bidder code to the bid object', () => { - expect(firstBid).to.have.property('bidderCode', 'getintent'); - expect(secondBid).to.have.property('bidderCode', 'getintent'); - expect(thirdBid).to.have.property('bidderCode', 'getintent'); - }); + it('Verify supported media types', () => { + expect(spec.supportedMediaTypes).to.have.lengthOf(2); + expect(spec.supportedMediaTypes[0]).to.equal('video'); + expect(spec.supportedMediaTypes[1]).to.equal('banner'); + }); - it('will respond to the video bid', () => { - expect(videoBid).to.have.property('vastUrl', 'http://test.com?pid=p4&tid=t3'); - expect(videoBid).to.have.property('cpm', 2.88); - expect(videoBid).to.have.property('width', '480'); - expect(videoBid).to.have.property('height', '352'); - }); + it('Verify if bid request valid', () => { + expect(spec.isBidRequestValid(bidRequests[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + expect(spec.isBidRequestValid({ params: { test: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { pid: 111, tid: 222 } })).to.equal(true); }); });