diff --git a/modules/aerservBidAdapter.js b/modules/aerservBidAdapter.js new file mode 100644 index 000000000000..64a02e773251 --- /dev/null +++ b/modules/aerservBidAdapter.js @@ -0,0 +1,117 @@ +import bidfactory from 'src/bidfactory'; +import bidmanager from 'src/bidmanager'; +import * as utils from 'src/utils'; +import {ajax} from 'src/ajax'; +import {STATUS} from 'src/constants'; +import adaptermanager from 'src/adaptermanager'; + +const BIDDER_CODE = 'aerserv'; + +const AerServAdapter = function AerServAdapter() { + const ENVIRONMENTS = { + local: '127.0.0.1:8080', + dev: 'dev-ads.aerserv.com', + stage: 'staging-ads.aerserv.com', + prod: 'ads.aerserv.com' + }; + + const BANNER_PATH = '/as/json/pbjs/v1?'; + const VIDEO_PATH = '/as/json/pbjsvast/v1?'; + const REQUIRED_PARAMS = ['plc']; + + function _isResponseValid(bidRequest, response) { + return ((bidRequest.mediaType === 'video' && response.vastUrl) || (bidRequest.mediaType !== 'video' && response.adm)) && + response.cpm && response.cpm > 0; + } + + function _createBid(bidRequest, response) { + if (_isResponseValid(bidRequest, response)) { + let bid = bidfactory.createBid(1, bidRequest); + bid.bidderCode = BIDDER_CODE; + bid.cpm = response.cpm; + bid.width = response.w; + bid.height = response.h; + if (bidRequest.mediaType === 'video') { + bid.vastUrl = response.vastUrl; + bid.descriptionUrl = response.vastUrl; + bid.mediaType = 'video'; + } else { + bid.ad = response.adm; + } + bidmanager.addBidResponse(bidRequest.placementCode, bid); + } else { + bidmanager.addBidResponse(bidRequest.placementCode, bidfactory.createBid(STATUS.NO_BID, bidRequest)); + } + } + + function _getFirstSize(sizes) { + let sizeObj = {}; + if (utils.isArray(sizes) && sizes.length > 0 && utils.isArray(sizes[0]) && sizes[0].length === 2) { + sizeObj['vpw'] = sizes[0][0]; + sizeObj['vph'] = sizes[0][1]; + } + return sizeObj; + } + + function _buildQueryParameters(bid, requestParams) { + Object.keys(bid.params).filter(param => param !== 'video') + .forEach(param => requestParams[param] = bid.params[param]); + + if (bid.mediaType === 'video') { + let videoDimensions = _getFirstSize(bid.sizes); + Object.keys(videoDimensions).forEach(param => requestParams[param] = videoDimensions[param]); + Object.keys(bid.params.video || {}).forEach(param => requestParams[param] = bid.params.video[param]); + } + + return utils.parseQueryStringParameters(requestParams); + } + + function _handleResponse(bidRequest) { + return response => { + if (!response && response.length <= 0) { + bidmanager.addBidResponse(bidRequest.placementCode, bidfactory.createBid(STATUS.NO_BID, bidRequest)); + utils.logError('Empty response'); + return; + } + + try { + response = JSON.parse(response); + } catch (e) { + bidmanager.addBidResponse(bidRequest.placementCode, bidfactory.createBid(STATUS.NO_BID, bidRequest)); + utils.logError('Invalid JSON in response'); + return; + } + + _createBid(bidRequest, response); + }; + } + + function _callBids(bidRequests) { + let currentUrl = (window.parent !== window) ? document.referrer : window.location.href; + currentUrl = currentUrl && encodeURIComponent(currentUrl); + + let bids = bidRequests.bids || []; + bids.forEach(bid => { + if (utils.hasValidBidRequest(bid.params, REQUIRED_PARAMS, BIDDER_CODE)) { + let env = ENVIRONMENTS[bid.params['env']] || ENVIRONMENTS['prod']; + let requestPath = bid.mediaType === 'video' ? VIDEO_PATH : BANNER_PATH; + let pageParameters = {url: currentUrl}; + let parameterStr = _buildQueryParameters(bid, pageParameters); + + let url = `//${env}${requestPath}${parameterStr}`; + utils.logMessage('sending request to: ' + url); + ajax(url, _handleResponse(bid), null, {withCredentials: true}); + } else { + bidmanager.addBidResponse(bid.placementCode, bidfactory.createBid(STATUS.NO_BID, bid)); + } + }); + } + + return { + callBids: _callBids + } +}; + +adaptermanager.registerBidAdapter(new AerServAdapter(), BIDDER_CODE, {supportedMediaTypes: ['video']}); + +module.exports = AerServAdapter; diff --git a/test/spec/modules/aerservBidAdapter_spec.js b/test/spec/modules/aerservBidAdapter_spec.js new file mode 100644 index 000000000000..be0f63930638 --- /dev/null +++ b/test/spec/modules/aerservBidAdapter_spec.js @@ -0,0 +1,213 @@ +import {expect} from 'chai'; +import AerServAdapter from 'modules/aerservBidAdapter'; +import bidmanager from 'src/bidmanager'; + +const BASE_REQUEST = JSON.stringify({ + bidderCode: 'aerserv', + requestId: 'a595eff7-d5a3-40f8-971c-5b4ef244ec53', + bidderRequestId: '1f8c8c03de01f9', + bids: [ + { + bidder: 'aerserv', + params: { + plc: '480', + }, + placementCode: 'adunit-1', + transactionId: 'a0e033af-f50c-4a7e-aeed-c01c5f709848', + sizes: [[300, 250], [300, 600]], + bidId: '2f4a69463b3bc9', + bidderRequestId: '1f8c8c03de01f9', + requestId: 'a595eff7-d5a3-40f8-971c-5b4ef244ec53' + } + ] +}); + +describe('AerServ Adapter', () => { + let adapter; + let bidmanagerStub; + + beforeEach(() => { + adapter = new AerServAdapter(); + bidmanagerStub = sinon.stub(bidmanager, 'addBidResponse'); + }); + + afterEach(() => { + bidmanager.addBidResponse.restore(); + }); + + describe('callBids()', () => { + let xhr; + let requests; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(() => { + xhr.restore(); + }); + + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + it('should not add bid responses with no bids to call', () => { + adapter.callBids({}); + + sinon.assert.notCalled(bidmanager.addBidResponse); + }); + + it('requires plc parameter to make request', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0].params = {}; + adapter.callBids(bidRequest); + expect(requests).to.be.empty; + }); + + it('sends requests to normal endpoint for non-video requests', () => { + adapter.callBids(JSON.parse(BASE_REQUEST)); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.include('/as/json/pbjs/v1'); + }); + + it('sends requests to video endpoint for video requests', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0]['mediaType'] = 'video'; + bidRequest.bids[0]['video'] = {}; + adapter.callBids(bidRequest); + expect(requests[0].url).to.include('/as/json/pbjsvast/v1'); + }); + + it('properly adds video parameters to the request', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0]['mediaType'] = 'video'; + bidRequest.bids[0].params['video'] = { videoParam: 'videoValue' }; + adapter.callBids(bidRequest); + expect(requests[0].url).to.include('videoParam=videoValue'); + }); + + it('parses the first size for video requests', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0]['mediaType'] = 'video'; + adapter.callBids(bidRequest); + expect(requests[0].url).to.include('vpw=300'); + expect(requests[0].url).to.include('vph=250'); + }); + + it('sends requests to production by default', () => { + adapter.callBids(JSON.parse(BASE_REQUEST)); + expect(requests[0].url).to.include('//ads.aerserv.com'); + }); + + it('sends requests to the specified endpoint when \'env\' is provided', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0].params['env'] = 'dev'; + adapter.callBids(bidRequest); + expect(requests[0].url).to.include('//dev-ads.aerserv.com'); + }); + }); + + describe('response handling', () => { + let server; + + beforeEach(() => { + server = sinon.fakeServer.create(); + }); + + afterEach(() => { + server.restore(); + }); + + it('responds with an empty bid without required parameters', () => { + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0].params = {}; + adapter.callBids(bidRequest); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + it('responds with an empty bid on empty response', () => { + server.respondWith(''); + + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + it('responds with an empty bid on un-parseable JSON response', () => { + server.respondWith('{\"bad\":\"json}'); + + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + it('responds with a valid bid returned ad', () => { + server.respondWith(JSON.stringify({cpm: 5, w: 320, h: 50, adm: 'sweet ad markup'})); + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(1); + }); + + it('responds with a valid bid from returned ad', () => { + server.respondWith(JSON.stringify({cpm: 5, w: 320, h: 50, vastUrl: 'sweet URL where VAST is at'})); + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0]['mediaType'] = 'video'; + bidRequest.bids[0]['video'] = {}; + adapter.callBids(bidRequest); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(1); + }); + + it('responds with empty bid if response has no ad', () => { + server.respondWith(JSON.stringify({error: 'no ads'})); + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + // things that should never occur + it('responds with empty bid if response has 0 or below cpm', () => { + server.respondWith(JSON.stringify({cpm: 0, w: 320, h: 50, adm: 'sweet ad markup'})); + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + it('responds with empty bid if response has no markup', () => { + server.respondWith(JSON.stringify({cpm: 5.0, w: 320, h: 50})); + adapter.callBids(JSON.parse(BASE_REQUEST)); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + + it('responds with an empty bid if response has no video markup', () => { + server.respondWith(JSON.stringify({cpm: 5, w: 320, h: 50})); + let bidRequest = JSON.parse(BASE_REQUEST); + bidRequest.bids[0]['mediaType'] = 'video'; + bidRequest.bids[0]['video'] = {}; + adapter.callBids(bidRequest); + server.respond(); + let bid = bidmanagerStub.getCall(0).args[1]; + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bid.getStatusCode()).to.equal(2); + }); + }); +});