diff --git a/adapters.json b/adapters.json index b9ec1b70cd2..bff61dd5cc9 100644 --- a/adapters.json +++ b/adapters.json @@ -1,4 +1,5 @@ [ + "a4g", "aardvark", "adblade", "adbund", diff --git a/src/adapters/a4g.js b/src/adapters/a4g.js new file mode 100644 index 00000000000..ee4afbde1ea --- /dev/null +++ b/src/adapters/a4g.js @@ -0,0 +1,170 @@ +const bidfactory = require('../bidfactory.js'), + bidmanager = require('../bidmanager.js'), + constants = require('../constants.json'), + adloader = require('../adloader'), + utils = require('../utils.js'); + +const A4G_BIDDER_CODE = 'a4g'; +const A4G_DEFAULT_BID_URL = '//ads.ad4game.com/v1/bid'; + +const IFRAME_NESTING_PARAM_NAME = 'if'; +const LOCATION_PARAM_NAME = 'siteurl'; +const ID_PARAM_NAME = 'id'; +const ZONE_ID_PARAM_NAME = 'zoneId'; +const SIZE_PARAM_NAME = 'size'; + +const A4G_SUPPORTED_PARAMS = [ + IFRAME_NESTING_PARAM_NAME, + LOCATION_PARAM_NAME, + ID_PARAM_NAME, + ZONE_ID_PARAM_NAME, + SIZE_PARAM_NAME +]; + +const ARRAY_PARAM_SEPARATOR = ';'; +const ARRAY_SIZE_SEPARATOR = ','; +const SIZE_SEPARATOR = 'x'; + +const JSONP_PARAM_NAME = 'jsonp'; + +function appendUrlParam(url, paramName, paramValue) { + const isQueryParams = url.indexOf('?') !== -1, + separator = isQueryParams ? '&' : '?'; + return url + separator + encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); +} + +function isInIframe(windowObj) { + return windowObj !== windowObj.parent; +} + +function getIframeInfo(window) { + let currentWindow = window, + iframeNestingLevel = 0, + hasExternalMeet = false, + hostHref = window.location.href; + + while (isInIframe(currentWindow)) { + currentWindow = currentWindow.parent; + + try { + if (hasExternalMeet) { + iframeNestingLevel = 1; + } else { + iframeNestingLevel++; + } + + hostHref = currentWindow.document.referrer || currentWindow.location.href; + } catch (e) { + hasExternalMeet = true; + } + } + + return { + nestingLevel: iframeNestingLevel, + hostHref: hostHref + }; +} + +function isValidStatus(status) { + return status === 200; +} + +function bidParamsToQuery(params) { + return A4G_SUPPORTED_PARAMS + .reduce((url, paramName) => paramName in params + ? appendUrlParam(url, paramName, params[paramName]) + : url, + ''); +} + +function buildBidRequestUrl(bidDeliveryUrl, params) { + return bidDeliveryUrl + bidParamsToQuery(params); +} + +function createBidRequest(bidRequest) { + return (status) => bidfactory.createBid(status, bidRequest); +} + +function mapBidToPrebidFormat(bidRequest, bid) { + const bidResponse = bidRequest(constants.STATUS.GOOD); + + bidResponse.bidderCode = A4G_BIDDER_CODE; + bidResponse.cpm = bid.cpm; + bidResponse.ad = bid.ad; + bidResponse.width = bid.width; + bidResponse.height = bid.height; + + return bidResponse; +} + +function mapBidErrorToPrebid(bidRequest) { + return bidRequest(constants.STATUS.NO_BID); +} + +function extractBidParams(bids) { + const idParams = []; + const sizeParams = []; + const zoneIds = []; + + let deliveryUrl = ''; + + for (let i = 0; i < bids.length; i++) { + const bid = bids[i]; + if (!deliveryUrl && typeof bid.params.deliveryUrl === 'string') { + deliveryUrl = bid.params.deliveryUrl; + } + idParams.push(bid.placementCode); + sizeParams.push(bid.sizes.map(size => size.join(SIZE_SEPARATOR)).join(ARRAY_SIZE_SEPARATOR)); + zoneIds.push(bid.params.zoneId); + } + + return [deliveryUrl, { + [ID_PARAM_NAME]: idParams.join(ARRAY_PARAM_SEPARATOR), + [ZONE_ID_PARAM_NAME]: zoneIds.join(ARRAY_PARAM_SEPARATOR), + [SIZE_PARAM_NAME]: sizeParams.join(ARRAY_PARAM_SEPARATOR) + }]; +} + +function a4gBidFactory() { + + function generateJsonpCallbackName() { + return '__A4G' + Date.now(); + } + + function jsonp(url, callback) { + const callbackName = generateJsonpCallbackName(), + jsnopUrl = appendUrlParam(url, JSONP_PARAM_NAME, callbackName); + + window[callbackName] = ({ status, response }) => { + !isValidStatus(status) + ? callback(new Error(`Failed fetching ad with status ${status}`), response) + : callback(null, response); + delete window[callbackName]; + }; + + adloader.loadScript(jsnopUrl); + } + + return { + callBids({ bids }) { + const bidRequests = bids.map(bid => createBidRequest(utils.getBidRequest(bid.bidId))); + const [ deliveryUrl, bidParams ] = extractBidParams(bids); + const { nestingLevel, hostHref } = getIframeInfo(window); + const envParams = { [IFRAME_NESTING_PARAM_NAME]: nestingLevel, [LOCATION_PARAM_NAME]: hostHref }; + const bidsRequestUrl = buildBidRequestUrl(deliveryUrl || A4G_DEFAULT_BID_URL, Object.assign({}, bidParams, envParams)); + + jsonp(bidsRequestUrl, (error, bidsResponse) => { + for (let i = 0; i < bidRequests.length; i++) { + const bidRequest = bidRequests[i], + placementCode = bids[i].placementCode; + + bidmanager.addBidResponse(placementCode, + error + ? mapBidErrorToPrebid(bidRequest) + : mapBidToPrebidFormat(bidRequest, bidsResponse[i])); + }}); + } + }; +} + +module.exports = a4gBidFactory; diff --git a/test/spec/adapters/a4g_spec.js b/test/spec/adapters/a4g_spec.js new file mode 100644 index 00000000000..32a17d7a2ed --- /dev/null +++ b/test/spec/adapters/a4g_spec.js @@ -0,0 +1,112 @@ +describe('a4g adapter tests', function () { + const expect = require('chai').expect; + const a4gBidFactory = require('src/adapters/a4g'); + const bidmanager = require('src/bidmanager'); + const adloader = require('src/adloader'); + const constants = require('src/constants.json'); + + function readJsonpCallbackName(url) { + return /&jsonp=([_a-zA-Z0-9]+)/.exec(url)[1]; + } + + let spyLoadScript, + spyAddBidResponse, + a4gAdapter; + + before(() => { + spyLoadScript = sinon.spy(adloader, 'loadScript'); + spyAddBidResponse = sinon.spy(bidmanager, 'addBidResponse'); + }); + + after(() => { + adloader.loadScript.restore(); + bidmanager.addBidResponse.restore(); + }); + + beforeEach(() => { + a4gAdapter = a4gBidFactory(); + }); + + it('should send proper jsonp request to default deliveryUrl', () => { + a4gAdapter.callBids({ bids: [{ + placementCode: 'pc1', + sizes: [[1, 2], [3, 4]], + params: { + zoneId: 1 + } + }, { + placementCode: 'pc2', + sizes: [[5, 6]], + params: { + zoneId: 2 + } + }]}); + + let targetUrl = spyLoadScript.lastCall.args[0]; + expect(targetUrl).to.contain('ads.ad4game.com/v1/bid'); + expect(targetUrl).to.contain('jsonp='); + expect(targetUrl).to.contain('id=pc1%3Bpc2'); + expect(targetUrl).to.contain('size=1x2%2C3x4%3B5x6'); + expect(targetUrl).to.contain('zoneId=1%3B2'); + }); + + it('should send proper jsonp request to deliveryUrl from 1st bid', () => { + a4gAdapter.callBids({ bids: [{ + placementCode: 'pc1', + sizes: [[1, 2], [3, 4]], + params: { + zoneId: 1, + deliveryUrl: 'new.test.delivery.com:8080/v105/new_bid' + } + }, { + placementCode: 'pc2', + sizes: [[5, 6]], + params: { + zoneId: 2, + deliveryUrl: 'nonused.test.delivery.com:8080/v105/new_bid' + } + }]}); + + let targetUrl = spyLoadScript.lastCall.args[0]; + expect(targetUrl).to.contain('new.test.delivery.com:8080/v105/new_bid'); + }); + + describe('on jsonp callback', () => { + let jsonpCallbackName; + + beforeEach(() => { + a4gAdapter.callBids({ bids: [{ + placementCode: 'pc1', + sizes: [[1, 2], [3, 4]], + params: { + zoneId: 1 + } + }]}); + jsonpCallbackName = readJsonpCallbackName(spyLoadScript.lastCall.args[0]); + }); + + it('should unregister', () => { + window[jsonpCallbackName]({status: 200, response: [{id: 'pc1', width: 1, height: 2, cpm: 1.0, ad: '' }]}); + expect(window[jsonpCallbackName]).to.not.be.a('function'); + }); + + it('should set all responses as bad if error received', () => { + window[jsonpCallbackName]({status: 400, response: []}); + let [placementCode, bid] = spyAddBidResponse.lastCall.args; + expect(placementCode).to.equal('pc1'); + expect(bid.getStatusCode()).to.equal(constants.STATUS.NO_BID); + }); + + it('should set all responses as good with appropriate values if ok', () => { + window[jsonpCallbackName]({status: 200, response: [{id: 'pc1', width: 1, height: 2, cpm: 1.0, ad: 'test' }]}); + let [placementCode, bid] = spyAddBidResponse.lastCall.args; + expect(placementCode).to.equal('pc1'); + + expect(bid.getStatusCode()).to.equal(constants.STATUS.GOOD); + expect(bid.cpm).to.equal(1); + expect(bid.ad).to.equal('test'); + expect(bid.width).to.equal(1); + expect(bid.height).to.equal(2); + }); + }); +});