From f0f093851a2d3b045adabc5d1dfa187d776a63aa Mon Sep 17 00:00:00 2001 From: Agustin Insua Date: Wed, 27 Dec 2017 16:44:51 -0300 Subject: [PATCH 1/3] Add eplanningBidAdapter --- modules/eplanningBidAdapter.js | 155 ++++++++ modules/eplanningBidAdapter.md | 28 ++ test/spec/modules/eplanningBidAdapter_spec.js | 345 ++++++++++++++++++ 3 files changed, 528 insertions(+) create mode 100644 modules/eplanningBidAdapter.js create mode 100644 modules/eplanningBidAdapter.md create mode 100644 test/spec/modules/eplanningBidAdapter_spec.js diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js new file mode 100644 index 00000000000..df55357d66c --- /dev/null +++ b/modules/eplanningBidAdapter.js @@ -0,0 +1,155 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'eplanning'; +const rnd = Math.random(); +const DEFAULT_SV = 'ads.us.e-planning.net'; +const DEFAULT_ISV = 'i.e-planning.net'; +const PARAMS = ['ci', 'sv', 't']; +const DOLLARS = 'USD'; +const NET_REVENUE = true; +const TTL = 120; +const NULL_SIZE = '1x1'; +const FILE = 'file'; + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function(bid) { + return Boolean(bid.params.ci) || Boolean(bid.params.t); + }, + buildRequests: function(bidRequests) { + const method = 'GET'; + const dfpClientId = '1'; + const sec = 'ROS'; + let url; + let params; + const urlConfig = getUrlConfig(bidRequests); + + if (urlConfig.t) { + url = urlConfig.isv + '/layers/t_pbjs_2.json'; + params = {}; + } else { + url = '//' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + (utils.getTopWindowLocation().hostname || FILE) + '/' + sec; + const referrerUrl = utils.getTopWindowReferrer(); + const spacesString = getSpacesString(bidRequests); + params = { + rnd: rnd, + e: spacesString, + ur: utils.getTopWindowUrl() || FILE, + r: 'pbjs', + pbv: '$prebid.version$', + }; + if (referrerUrl) { + params.fr = referrerUrl; + } + } + + return { + method: method, + url: url, + data: params, + adUnitToBidId: getBidIdMap(bidRequests), + }; + }, + interpretResponse: function(serverResponse, request) { + const response = serverResponse.body; + let bidResponses = []; + + if (response && !utils.isEmpty(response.sp)) { + response.sp.forEach(space => { + if (!utils.isEmpty(space.a)) { + space.a.forEach(ad => { + const bidResponse = { + requestId: request.adUnitToBidId[space.k], + cpm: ad.pr, + width: ad.w, + height: ad.h, + ad: ad.adm, + ttl: TTL, + creativeId: ad.crid, + netRevenue: NET_REVENUE, + currency: DOLLARS, + }; + bidResponses.push(bidResponse); + }); + } + }); + } + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + const response = !utils.isEmpty(serverResponses) && serverResponses[0].body; + + if (response && !utils.isEmpty(response.cs)) { + const responseSyncs = response.cs; + responseSyncs.forEach(sync => { + if (typeof sync === 'string' && syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: 'sync', + }); + } else if (typeof sync === 'object' && sync.ifr && syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: sync.u, + }) + } + }); + } + + return syncs; + }, +} + +function cleanName(name) { + return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)/g, '_').replace(/^_+|_+$/g, ''); +} +function getUrlConfig(bidRequests) { + if (isTestRequest(bidRequests)) { + return getTestConfig(bidRequests.filter(br => br.params.t)); + } + + let config = {}; + bidRequests.forEach(bid => { + PARAMS.forEach(param => { + if (bid.params[param] && !config[param]) { + config[param] = bid.params[param]; + } + }); + }); + + if (config.sv) { + config.sv = '//' + config.sv; + } + + return config; +} +function isTestRequest(bidRequests) { + let isTest = false; + bidRequests.forEach(bid => isTest = bid.params.t); + return isTest; +} +function getTestConfig(bidRequests) { + let isv; + bidRequests.forEach(br => isv = isv || br.params.isv); + return { + t: true, + isv: '//' + (isv || DEFAULT_ISV) + }; +} +function getSpacesString(bids) { + const spacesString = bids.map(bid => + cleanName(bid.adUnitCode) + ':' + (bid.sizes && bid.sizes.length ? utils.parseSizesInput(bid.sizes).join(',') : NULL_SIZE) + ).join('+'); + + return spacesString; +} +function getBidIdMap(bidRequests) { + let map = {}; + bidRequests.forEach(bid => map[cleanName(bid.adUnitCode)] = bid.bidId); + return map; +} + +registerBidder(spec); diff --git a/modules/eplanningBidAdapter.md b/modules/eplanningBidAdapter.md new file mode 100644 index 00000000000..eab05f9af3e --- /dev/null +++ b/modules/eplanningBidAdapter.md @@ -0,0 +1,28 @@ +# Overview + +``` +Module Name: E-Planning Bid Adapter +Module Type: Bidder Adapter +Maintainer: ainsua@e-planning.net +``` + +# Description + +Connects to E-Planning exchange for bids. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'div-gpt-ad-1460505748561-0', + sizes: [[300, 250]], + bids: [{ + bidder: 'eplanning', + params: { + t: 1 + } + }] + } + ]; +``` diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js new file mode 100644 index 00000000000..9ee25ced972 --- /dev/null +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -0,0 +1,345 @@ +import { expect } from 'chai'; +import { spec } from 'modules/eplanningBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +describe('E-Planning Adapter', () => { + const adapter = newBidder('spec'); + const CI = '12345'; + const ADUNIT_CODE = 'adunit-code'; + const ADUNIT_CODE2 = 'adunit-code-dos'; + const CLEAN_ADUNIT_CODE2 = 'adunitcodedos'; + const CLEAN_ADUNIT_CODE = 'adunitcode'; + const BID_ID = '123456789'; + const BID_ID2 = '987654321'; + const CPM = 1.3; + const W = '300'; + const H = '250'; + const ADM = '
This is an ad
'; + const I_ID = '7854abc56248f873'; + const CRID = '1234567890'; + const TEST_ISV = 'leles.e-planning.net'; + const validBid = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE, + 'sizes': [[300, 250], [300, 600]], + }; + const validBid2 = { + 'bidder': 'eplanning', + 'bidId': BID_ID2, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE2, + 'sizes': [[300, 250], [300, 600]], + }; + const testBid = { + 'bidder': 'eplanning', + 'params': { + 't': 1, + 'isv': TEST_ISV + }, + 'adUnitCode': ADUNIT_CODE, + 'sizes': [[300, 250], [300, 600]], + }; + const invalidBid = { + 'bidder': 'eplanning', + 'params': { + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + }; + const response = { + body: { + 'sI': { + 'k': '12345' + }, + 'sec': { + 'k': 'ROS' + }, + 'sp': [{ + 'k': CLEAN_ADUNIT_CODE, + 'a': [{ + 'adm': ADM, + 'id': '7854abc56248f874', + 'i': I_ID, + 'fi': '7854abc56248f872', + 'ip': '45621afd87462104', + 'w': W, + 'h': H, + 'crid': CRID, + 'pr': CPM + }], + }], + 'cs': [ + 'http://a-sync-url.com/', + { + 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east', + 'ifr': true + } + ] + } + }; + const responseWithTwoAdunits = { + body: { + 'sI': { + 'k': '12345' + }, + 'sec': { + 'k': 'ROS' + }, + 'sp': [{ + 'k': CLEAN_ADUNIT_CODE, + 'a': [{ + 'adm': ADM, + 'id': '7854abc56248f874', + 'i': I_ID, + 'fi': '7854abc56248f872', + 'ip': '45621afd87462104', + 'w': W, + 'h': H, + 'crid': CRID, + 'pr': CPM + }] + }, { + 'k': CLEAN_ADUNIT_CODE2, + 'a': [{ + 'adm': ADM, + 'id': '7854abc56248f874', + 'i': I_ID, + 'fi': '7854abc56248f872', + 'ip': '45621afd87462104', + 'w': W, + 'h': H, + 'crid': CRID, + 'pr': CPM + }], + }, + ], + 'cs': [ + 'http://a-sync-url.com/', + { + 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east', + 'ifr': true + } + ] + } + }; + const responseWithNoAd = { + body: { + 'sI': { + 'k': '12345' + }, + 'sec': { + 'k': 'ROS' + }, + 'sp': [{ + 'k': 'spname', + }], + 'cs': [ + 'http://a-sync-url.com/', + { + 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east', + 'ifr': true + } + ] + } + }; + const responseWithNoSpace = { + body: { + 'sI': { + 'k': '12345' + }, + 'sec': { + 'k': 'ROS' + }, + 'cs': [ + 'http://a-sync-url.com/', + { + 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east', + 'ifr': true + } + ] + } + }; + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + it('should return true when bid has ci parameter', () => { + expect(spec.isBidRequestValid(validBid)).to.equal(true); + }); + + it('should return false when bid does not have ci parameter and is not a test bid', () => { + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return true when bid does not have ci parameter but is a test bid'), () => { + expect(spec.isBidRequestValid(testBid).to.equal(true)); + } + }); + + describe('buildRequests', () => { + let bidRequests = [validBid]; + + it('should create the url correctly', () => { + const url = spec.buildRequests(bidRequests).url; + expect(url).to.equal('//ads.us.e-planning.net/hb/1/' + CI + '/1/localhost/ROS'); + }); + + it('should return GET method', () => { + const method = spec.buildRequests(bidRequests).method; + expect(method).to.equal('GET'); + }); + + it('should return r parameter with value pbjs', () => { + const r = spec.buildRequests(bidRequests).data.r; + expect(r).to.equal('pbjs'); + }); + + it('should return pbv parameter with value prebid version', () => { + const pbv = spec.buildRequests(bidRequests).data.pbv; + expect(pbv).to.equal('$prebid.version$'); + }); + + it('should return e parameter with value according to the adunit sizes', () => { + const e = spec.buildRequests(bidRequests).data.e; + expect(e).to.equal(CLEAN_ADUNIT_CODE + ':300x250,300x600'); + }); + + it('should return correct e parameter with more than one adunit', () => { + const NEW_CODE = ADUNIT_CODE + '2'; + const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; + const anotherBid = { + 'bidder': 'eplanning', + 'params': { + 'ci': CI, + }, + 'adUnitCode': NEW_CODE, + 'sizes': [[100, 100]], + }; + bidRequests.push(anotherBid); + + const e = spec.buildRequests(bidRequests).data.e; + expect(e).to.equal(CLEAN_ADUNIT_CODE + ':300x250,300x600+' + CLEAN_NEW_CODE + ':100x100'); + }); + + it('should return correct e parameter when the adunit has no size', () => { + const noSizeBid = { + 'bidder': 'eplanning', + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE, + }; + + const e = spec.buildRequests([noSizeBid]).data.e; + expect(e).to.equal(CLEAN_ADUNIT_CODE + ':1x1'); + }); + + it('should return ur parameter with current window url', () => { + const ur = spec.buildRequests(bidRequests).data.ur; + expect(ur).to.equal(utils.getTopWindowUrl()); + }); + + it('should return fr parameter when there is a referrer', () => { + const referrer = 'thisisafakereferrer'; + const stubGetReferrer = sinon.stub(utils, 'getTopWindowReferrer').returns(referrer); + after(() => stubGetReferrer.restore()); + + const fr = spec.buildRequests(bidRequests).data.fr; + expect(fr).to.equal(referrer); + }); + + it('should return the testing url when the request has the t parameter', () => { + const url = spec.buildRequests([testBid]).url; + const expectedUrl = '//' + TEST_ISV + '/layers/t_pbjs_2.json'; + expect(url).to.equal(expectedUrl); + }); + }); + + describe('interpretResponse', () => { + it('should return an empty array when there is no ads in the response', () => { + const bidResponses = spec.interpretResponse(responseWithNoAd); + expect(bidResponses).to.be.empty; + }); + + it('should return an empty array when there is no spaces in the response', () => { + const bidResponses = spec.interpretResponse(responseWithNoSpace); + expect(bidResponses).to.be.empty; + }); + + it('should correctly map the parameters in the response', () => { + const bidResponse = spec.interpretResponse(response, { adUnitToBidId: { [CLEAN_ADUNIT_CODE]: BID_ID } })[0]; + const expectedResponse = { + requestId: BID_ID, + cpm: CPM, + width: W, + height: H, + ad: ADM, + ttl: 120, + creativeId: CRID, + netRevenue: true, + currency: 'USD', + }; + expect(bidResponse).to.deep.equal(expectedResponse); + }); + }); + + describe('getUserSyncs', () => { + const sOptionsAllEnabled = { + pixelEnabled: true, + iframeEnabled: true + }; + const sOptionsAllDisabled = { + pixelEnabled: false, + iframeEnabled: false + }; + const sOptionsOnlyPixel = { + pixelEnabled: true, + iframeEnabled: false + }; + const sOptionsOnlyIframe = { + pixelEnabled: false, + iframeEnabled: true + }; + + it('should return an empty array if the response has no syncs', () => { + const noSyncsResponse = { cs: [] }; + const syncs = spec.getUserSyncs(sOptionsAllEnabled, [noSyncsResponse]); + expect(syncs).to.be.empty; + }); + + it('should return an empty array if there is no sync options enabled', () => { + const syncs = spec.getUserSyncs(sOptionsAllDisabled, [response]); + expect(syncs).to.be.empty; + }); + + it('should only return pixels if iframe is not enabled', () => { + const syncs = spec.getUserSyncs(sOptionsOnlyPixel, [response]); + syncs.forEach(sync => expect(sync.type).to.equal('image')); + }); + + it('should only return iframes if pixel is not enabled', () => { + const syncs = spec.getUserSyncs(sOptionsOnlyIframe, [response]); + syncs.forEach(sync => expect(sync.type).to.equal('iframe')); + }); + }); + + describe('adUnits mapping to bidId', () => { + it('should correctly map the bidId to the adunit', () => { + const requests = spec.buildRequests([validBid, validBid2]); + const responses = spec.interpretResponse(responseWithTwoAdunits, requests); + expect(responses[0].requestId).to.equal(BID_ID); + expect(responses[1].requestId).to.equal(BID_ID2); + }); + }); +}); From 172dac4b78ff3307c155c58b0da64af9bc6b7fe3 Mon Sep 17 00:00:00 2001 From: Agustin Insua Date: Thu, 28 Dec 2017 15:23:17 -0300 Subject: [PATCH 2/3] Add new parameter to request --- modules/eplanningBidAdapter.js | 1 + modules/eplanningBidAdapter.md | 23 ++++++++----------- test/spec/modules/eplanningBidAdapter_spec.js | 5 ++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index df55357d66c..2e80ea19d41 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -38,6 +38,7 @@ export const spec = { ur: utils.getTopWindowUrl() || FILE, r: 'pbjs', pbv: '$prebid.version$', + ncb: '1' }; if (referrerUrl) { params.fr = referrerUrl; diff --git a/modules/eplanningBidAdapter.md b/modules/eplanningBidAdapter.md index eab05f9af3e..b6cfbb535b6 100644 --- a/modules/eplanningBidAdapter.md +++ b/modules/eplanningBidAdapter.md @@ -12,17 +12,14 @@ Connects to E-Planning exchange for bids. # Test Parameters ``` -var adUnits = [ - // Banner adUnit - { - code: 'div-gpt-ad-1460505748561-0', - sizes: [[300, 250]], - bids: [{ - bidder: 'eplanning', - params: { - t: 1 - } - }] - } - ]; +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[300, 250]], + bids: [{ + bidder: 'eplanning', + params: { + t: 1 + } + }] +}]; ``` diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 9ee25ced972..2ec7f482edd 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -264,6 +264,11 @@ describe('E-Planning Adapter', () => { const expectedUrl = '//' + TEST_ISV + '/layers/t_pbjs_2.json'; expect(url).to.equal(expectedUrl); }); + + it('should return the parameter ncb with value 1', () => { + const ncb = spec.buildRequests(bidRequests).data.ncb; + expect(ncb).to.equal('1'); + }); }); describe('interpretResponse', () => { From e2f731ce0bdcd19998db0d7b65691da317da8c07 Mon Sep 17 00:00:00 2001 From: Agustin Insua Date: Thu, 4 Jan 2018 10:11:22 -0300 Subject: [PATCH 3/3] Fix sync URL parameter --- modules/eplanningBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 2e80ea19d41..dfc5f514cf3 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -89,7 +89,7 @@ export const spec = { if (typeof sync === 'string' && syncOptions.pixelEnabled) { syncs.push({ type: 'image', - url: 'sync', + url: sync, }); } else if (typeof sync === 'object' && sync.ifr && syncOptions.iframeEnabled) { syncs.push({