diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js new file mode 100644 index 00000000000..ccb0287a866 --- /dev/null +++ b/modules/adxcgBidAdapter.js @@ -0,0 +1,139 @@ +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'; + +/** + * Adapter for requesting bids from Adxcg + * updated from latest prebid repo on 2017.08.30 + */ +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); + } + + 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; + } + }); + } + + bid.width = adxcgBidReponse.width; + bid.height = adxcgBidReponse.height; + + utils.logMessage(`submitting bid[${bidRequest.placementCode}]: ${JSON.stringify(bid)}`); + bidmanager.addBidResponse(bidRequest.placementCode, bid); + }); + + 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)); + }); + }; + + return { + callBids: _callBids + }; +}; + +adaptermanager.registerBidAdapter(new AdxcgAdapter(), 'adxcg', { + supportedMediaTypes: ['video', 'native'] +}); + +module.exports = AdxcgAdapter; diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js new file mode 100644 index 00000000000..fa55bf92e2e --- /dev/null +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -0,0 +1,212 @@ +import { expect } from 'chai'; +import Adapter from 'modules/adxcgBidAdapter'; +import bidmanager from 'src/bidmanager'; +import * as url from 'src/url'; + +const REQUEST = { + 'bidderCode': 'adxcg', + 'bids': [ + { + 'bidder': 'adxcg', + 'params': { + 'adzoneid': '1', + }, + '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'] + } +}] + +describe('AdxcgAdapter', () => { + let adapter; + + beforeEach(() => adapter = new Adapter()); + + describe('request function', () => { + let xhr; + let requests; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(() => xhr.restore()); + + it('creates a valid adxcg request url', () => { + adapter.callBids(REQUEST); + + let parsedRequestUrl = url.parse(requests[0].url); + + expect(parsedRequestUrl.hostname).to.equal('ad-emea.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.adzoneid).to.equal('1'); + expect(query.format).to.equal('300x250|640x360|1x1'); + expect(query.jsonp).to.be.empty; + expect(query.prebidBidIds).to.equal('84ab500420319d'); + }); + }); + + describe('response handler', () => { + let server; + + beforeEach(() => { + server = sinon.fakeServer.create(); + sinon.stub(bidmanager, 'addBidResponse'); + }); + + afterEach(() => { + server.restore() + bidmanager.addBidResponse.restore(); + }); + + 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(''); + }); + + 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'); + }); + + 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'); + }); + + it('handles nobid responses', () => { + server.respondWith('[]'); + + adapter.callBids(REQUEST); + server.respond(); + sinon.assert.calledOnce(bidmanager.addBidResponse); + + const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; + expect(bidResponse.statusMessage).to.equal('Bid returned empty or error response'); + }); + }); +});