From 6cfaa5cc8b5c1e3830ca0afc748e8372bfe8a8ff Mon Sep 17 00:00:00 2001 From: Bill Newman Date: Tue, 23 Jan 2018 21:19:08 +0200 Subject: [PATCH] Colossus SSP header bidding adapter 1.0.0 (#2029) * create adapter colossusssp * Trigger for current build rerun on Travis CI --- modules/colossussspBidAdapter.js | 132 ++++++++++++++++++ modules/colossussspBidAdapter.md | 26 ++++ .../modules/colossussspBidAdapter_spec.js | 119 ++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 modules/colossussspBidAdapter.js create mode 100644 modules/colossussspBidAdapter.md create mode 100644 test/spec/modules/colossussspBidAdapter_spec.js diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js new file mode 100644 index 00000000000..df011bc102d --- /dev/null +++ b/modules/colossussspBidAdapter.js @@ -0,0 +1,132 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +const BIDDER_CODE = 'colossusssp'; +const URL = '//colossusssp.com/?c=o&m=multi'; +const URL_SYNC = '//colossusssp.com/?c=o&m=cookie'; + +let sizeObj = { + '468x60': 1, + '728x90': 2, + '300x600': 10, + '300x250': 15, + '300x100': 19, + '320x50': 43, + '300x50': 44, + '300x300': 48, + '300x1050': 54, + '970x90': 55, + '970x250': 57, + '1000x90': 58, + '320x80': 59, + '640x480': 65, + '320x480': 67, + '320x320': 72, + '320x160': 73, + '480x300': 83, + '970x310': 94, + '970x210': 96, + '480x320': 101, + '768x1024': 102, + '1000x300': 113, + '320x100': 117, + '800x250': 118, + '200x600': 119 +}; + +utils._each(sizeObj, (item, key) => sizeObj[item] = key); + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + return (!isNaN(bid.params.placement_id) && + ((bid.params.sizes !== undefined && bid.params.sizes.length > 0 && bid.params.sizes.some((sizeIndex) => sizeObj[sizeIndex] !== undefined)) || + (bid.sizes !== undefined && bid.sizes.length > 0 && bid.sizes.map((size) => `${size[0]}x${size[1]}`).some((size) => sizeObj[size] !== undefined)))); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: (validBidRequests) => { + let winTop = window; + try { + window.top.location.toString(); + winTop = window.top; + } catch (e) { + utils.logMessage(e); + }; + let location = utils.getTopWindowLocation(); + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + 'secure': location.protocol === 'https:' ? 1 : 0, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + for (let i = 0; i < validBidRequests.length; i++) { + let bid = validBidRequests[i]; + let placement = {}; + placement['placementId'] = bid.params.placement_id; + placement['bidId'] = bid.bidId; + placement['sizes'] = bid.sizes; + placements.push(placement); + } + return { + method: 'POST', + url: URL, + data: request + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse) => { + let response = []; + try { + serverResponse = serverResponse.body; + for (let i = 0; i < serverResponse.length; i++) { + let resItem = serverResponse[i]; + if (resItem.width && !isNaN(resItem.width) && + resItem.height && !isNaN(resItem.height) && + resItem.requestId && typeof resItem.requestId === 'string' && + resItem.cpm && !isNaN(resItem.cpm) && + resItem.ad && typeof resItem.ad === 'string' && + resItem.ttl && !isNaN(resItem.ttl) && + resItem.creativeId && typeof resItem.creativeId === 'string' && + resItem.netRevenue && typeof resItem.netRevenue === 'boolean' && + resItem.currency && typeof resItem.currency === 'string') { + response.push(resItem); + } + } + } catch (e) { + utils.logMessage(e); + }; + return response; + }, + + getUserSyncs: () => { + return [{ + type: 'image', + url: URL_SYNC + }]; + } +}; + +registerBidder(spec); diff --git a/modules/colossussspBidAdapter.md b/modules/colossussspBidAdapter.md new file mode 100644 index 00000000000..9a5b9a0fe39 --- /dev/null +++ b/modules/colossussspBidAdapter.md @@ -0,0 +1,26 @@ +# Overview + +``` +Module Name: Colossus SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@colossusmediallc.com +``` + +# Description + +Module that connects to Colossus SSP demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'placementid_0', + sizes: [[300, 250]], + bids: [{ + bidder: 'colossusssp', + params: { + placement_id: 0 + } + }] + } + ]; +``` diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js new file mode 100644 index 00000000000..e14d2f27b42 --- /dev/null +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -0,0 +1,119 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/colossussspBidAdapter'; + +describe('ColossussspAdapter', () => { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'colossusssp', + bidderRequestId: '145e1d6a7837c9', + params: { + placement_id: 0 + }, + placementCode: 'placementid_0', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + sizes: [[300, 250]], + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62' + }; + + describe('isBidRequestValid', () => { + it('Should return true when placement_id can be cast to a number, and when at least one of the sizes passed is allowed', () => { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when placement_id is not a number', () => { + bid.params.placement_id = 'aaa'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false when the sizes are not allowed', () => { + bid.sizes = [[1, 1]]; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', () => { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', () => { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', () => { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', () => { + expect(serverRequest.url).to.equal('//colossusssp.com/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', () => { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placements = data['placements']; + for (let i = 0; i < placements.length; i++) { + let placement = placements[i]; + expect(placement).to.have.all.keys('placementId', 'bidId', 'sizes'); + expect(placement.placementId).to.be.a('number'); + expect(placement.bidId).to.be.a('string'); + expect(placement.sizes).to.be.an('array'); + } + }); + it('Returns empty data if no valid requests are passed', () => { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', () => { + let resObject = { + body: [ { + requestId: '123', + cpm: 0.3, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + } ] + }; + let serverResponses = spec.interpretResponse(resObject); + it('Returns an array of valid server responses if response object is valid', () => { + expect(serverResponses).to.be.an('array').that.is.not.empty; + for (let i = 0; i < serverResponses.length; i++) { + let dataItem = serverResponses[i]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency'); + expect(dataItem.requestId).to.be.a('string'); + expect(dataItem.cpm).to.be.a('number'); + expect(dataItem.width).to.be.a('number'); + expect(dataItem.height).to.be.a('number'); + expect(dataItem.ad).to.be.a('string'); + expect(dataItem.ttl).to.be.a('number'); + expect(dataItem.creativeId).to.be.a('string'); + expect(dataItem.netRevenue).to.be.a('boolean'); + expect(dataItem.currency).to.be.a('string'); + } + it('Returns an empty array if invalid response is passed', () => { + serverResponses = spec.interpretResponse('invalid_response'); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + }); + + describe('getUserSyncs', () => { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', () => { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('//colossusssp.com/?c=o&m=cookie'); + }); + }); +});