Skip to content

Commit

Permalink
Add AerServ Adapter (#1538)
Browse files Browse the repository at this point in the history
* implemented AerServ adapter

* added unit tests, slight refactor/formatting of adapter code, added to adapters json

* implemented AerServ adapter

* added unit tests, slight refactor/formatting of adapter code, added to adapters json

* feature necromancy! post-merge fixes to account for new structure

* last minute addition of some video parameter logic + some name refactoring
  • Loading branch information
cormorinth authored and matthewlane committed Aug 31, 2017
1 parent df168ef commit 1fd757a
Show file tree
Hide file tree
Showing 2 changed files with 330 additions and 0 deletions.
117 changes: 117 additions & 0 deletions modules/aerservBidAdapter.js
Original file line number Diff line number Diff line change
@@ -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;
213 changes: 213 additions & 0 deletions test/spec/modules/aerservBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 1fd757a

Please sign in to comment.