diff --git a/adapters.json b/adapters.json index 3aa09851a33..0ebf6ba9319 100644 --- a/adapters.json +++ b/adapters.json @@ -30,6 +30,7 @@ "smartyads", "smartadserver", "sekindoUM", + "serverbid", "sonobi", "sovrn", "springserve", diff --git a/src/adapters/serverbid.js b/src/adapters/serverbid.js new file mode 100644 index 00000000000..74b731bbe3f --- /dev/null +++ b/src/adapters/serverbid.js @@ -0,0 +1,163 @@ +import Adapter from 'src/adapters/adapter'; +import bidfactory from 'src/bidfactory'; +import bidmanager from 'src/bidmanager'; +import * as utils from 'src/utils'; +import { ajax } from 'src/ajax'; + +const ServerBidAdapter = function ServerBidAdapter() { + + const baseAdapter = Adapter.createNew('serverbid'); + + const BASE_URI = '//e.serverbid.com/api/v2'; + + const sizeMap = [null, + "120x90", + "120x90", + "468x60", + "728x90", + "300x250", + "160x600", + "120x600", + "300x100", + "180x150", + "336x280", + "240x400", + "234x60", + "88x31", + "120x60", + "120x240", + "125x125", + "220x250", + "250x250", + "250x90", + "0x0", + "200x90", + "300x50", + "320x50", + "320x480", + "185x185", + "620x45", + "300x125", + "800x250" + ]; + + const bidIds = []; + + baseAdapter.callBids = function(params) { + + if (params && params.bids && utils.isArray(params.bids) && params.bids.length) { + + const data = { + placements: [], + time: Date.now(), + user: {}, + url: utils.getTopWindowUrl(), + referrer: document.referrer, + enableBotFiltering: true, + includePricingData: true + }; + + const bids = params.bids || []; + for (let i = 0; i < bids.length; i++) { + const bid = bids[i]; + + bidIds.push(bid.bidId); + + const bid_data = { + networkId: bid.params.networkId, + siteId: bid.params.siteId, + zoneIds: bid.params.zoneIds, + campaignId: bid.params.campaignId, + flightId: bid.params.flightId, + adId: bid.params.adId, + divName: bid.bidId, + adTypes: bid.adTypes || getSize(bid.sizes) + }; + + if (bid_data.networkId && bid_data.siteId) { + data.placements.push(bid_data); + } + + } + + if (data.placements.length) { + ajax(BASE_URI, _responseCallback, JSON.stringify(data), { method: 'POST', withCredentials: true, contentType: 'application/json' }); + } + + } + + }; + + function _responseCallback(result) { + + let bid; + let bidId; + let bidObj; + let bidCode; + let placementCode; + + try { + result = JSON.parse(result); + } catch (error) { + utils.logError(error); + } + + for (let i = 0; i < bidIds.length; i++) { + + bidId = bidIds[i]; + bidObj = utils.getBidRequest(bidId); + bidCode = bidObj.bidder; + placementCode = bidObj.placementCode; + + if (result) { + const decision = result.decisions && result.decisions[bidId]; + const price = decision && decision.pricing && decision.pricing.clearPrice; + + if (decision && price) { + bid = bidfactory.createBid(1, bidObj); + bid.bidderCode = bidCode; + bid.cpm = price; + bid.width = decision.width; + bid.height = decision.height; + bid.ad = retrieveAd(decision); + } else { + bid = bidfactory.createBid(2, bidObj); + bid.bidderCode = bidCode; + } + + } else { + bid = bidfactory.createBid(2, bidObj); + bid.bidderCode = bidCode; + } + bidmanager.addBidResponse(placementCode, bid); + } + } + + function retrieveAd(decision) { + return decision.contents && decision.contents[0] && decision.contents[0].body + utils.createTrackPixelHtml(decision.impressionUrl); + } + + function getSize(sizes) { + const result = []; + sizes.forEach(function(size) { + const index = sizeMap.indexOf(size[0] + "x" + size[1]); + if (index >= 0) { + result.push(index); + } + }); + return result; + } + + // Export the `callBids` function, so that Prebid.js can execute + // this function when the page asks to send out bid requests. + return { + callBids: baseAdapter.callBids + }; + +}; + +ServerBidAdapter.createNew = function() { + return new ServerBidAdapter(); +}; + +module.exports = ServerBidAdapter; \ No newline at end of file diff --git a/test/spec/adapters/serverbid_spec.js b/test/spec/adapters/serverbid_spec.js new file mode 100644 index 00000000000..3a7408c34e5 --- /dev/null +++ b/test/spec/adapters/serverbid_spec.js @@ -0,0 +1,178 @@ +/* jshint -W024 */ +/* jshint expr:true */ + +import { expect } from 'chai'; +import Adapter from 'src/adapters/serverbid'; +import bidmanager from 'src/bidmanager'; +import * as utils from 'src/utils'; + +const ENDPOINT = '//e.serverbid.com/api/v2'; + +const REQUEST = { + "bidderCode": "serverbid", + "requestId": "a4713c32-3762-4798-b342-4ab810ca770d", + "bidderRequestId": "109f2a181342a9", + "bids": [{ + "bidder": "serverbid", + "params": { + "networkId": 9969, + "siteId": 730181 + }, + "placementCode": "div-gpt-ad-1487778092495-0", + "sizes": [ + [728, 90], + [970, 90] + ], + "bidId": "2b0f82502298c9", + "bidderRequestId": "109f2a181342a9", + "requestId": "a4713c32-3762-4798-b342-4ab810ca770d" + }], + "start": 1487883186070, + "auctionStart": 1487883186069, + "timeout": 3000 +}; + +const RESPONSE = { + "user": { "key": "ue1-2d33e91b71e74929b4aeecc23f4376f1" }, + "decisions": { + "2b0f82502298c9": { + "adId": 2364764, + "creativeId": 1950991, + "flightId": 2788300, + "campaignId": 542982, + "clickUrl": "http://e.serverbid.com/r", + "impressionUrl": "http://e.serverbid.com/i.gif", + "contents": [{ + "type": "html", + "body": "", + "data": { + "height": 90, + "width": 728, + "imageUrl": "http://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif", + "fileName": "b0ab77db8a7848c8b78931aed022a5ef.gif" + }, + "template": "image" + }], + "height": 90, + "width": 728, + "events": [], + "pricing":{"price":0.5,"clearPrice":0.5,"revenue":0.0005,"rateType":2,"eCPM":0.5} + }, + } +}; + +describe('serverbidAdapter', () => { + + let adapter; + + beforeEach(() => adapter = Adapter.createNew()); + + describe('request function', () => { + + let xhr; + let requests; + let pbConfig; + + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + pbConfig = REQUEST; + //just a single slot + pbConfig.bids = [pbConfig.bids[0]]; + }); + + afterEach(() => xhr.restore()); + + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + it('requires paramaters to make request', () => { + adapter.callBids({}); + expect(requests).to.be.empty; + }); + + it('requires networkId and siteId', () => { + let backup = pbConfig.bids[0].params; + pbConfig.bids[0].params = { networkId: 1234 }; //no hbid + adapter.callBids(pbConfig); + expect(requests).to.be.empty; + + pbConfig.bids[0].params = { siteId: 1234 }; //no placementid + adapter.callBids(pbConfig); + expect(requests).to.be.empty; + + pbConfig.bids[0].params = backup; + }); + + it('sends bid request to ENDPOINT via POST', () => { + adapter.callBids(pbConfig); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('POST'); + }); + }); + + describe('response handler', () => { + + let server; + + beforeEach(() => { + server = sinon.fakeServer.create(); + sinon.stub(bidmanager, 'addBidResponse'); + sinon.stub(utils, "getBidRequest").returns(REQUEST); + }); + + afterEach(() => { + server.restore(); + bidmanager.addBidResponse.restore(); + utils.getBidRequest.restore(); + }); + + it('registers bids', () => { + server.respondWith(JSON.stringify(RESPONSE)); + + adapter.callBids(REQUEST); + server.respond(); + sinon.assert.calledOnce(bidmanager.addBidResponse); + + const response = bidmanager.addBidResponse.firstCall.args[1]; + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('cpm'); + expect(response.cpm).to.be.above(0); + }); + + it('handles nobid responses', () => { + server.respondWith(JSON.stringify({ + "decisions": [] + })); + + adapter.callBids(REQUEST); + server.respond(); + sinon.assert.calledOnce(bidmanager.addBidResponse); + + const response = bidmanager.addBidResponse.firstCall.args[1]; + expect(response).to.have.property( + 'statusMessage', + 'Bid returned empty or error response' + ); + }); + + it('handles JSON.parse errors', () => { + server.respondWith(''); + + adapter.callBids(REQUEST); + server.respond(); + sinon.assert.calledOnce(bidmanager.addBidResponse); + + const response = bidmanager.addBidResponse.firstCall.args[1]; + expect(response).to.have.property( + 'statusMessage', + 'Bid returned empty or error response' + ); + }); + + }); + +}); \ No newline at end of file