diff --git a/modules/vubleBidAdapter.js b/modules/vubleBidAdapter.js new file mode 100644 index 000000000000..c995705c9577 --- /dev/null +++ b/modules/vubleBidAdapter.js @@ -0,0 +1,135 @@ +// Vuble Adapter + +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'vuble'; + +const ENVS = ['com', 'net']; +const CURRENCIES = { + com: 'EUR', + net: 'USD' +}; +const TTL = 60; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['video'], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (utils.isEmpty(bid.sizes) || utils.parseSizesInput(bid.sizes).length == 0) { + return false; + } + + if (!utils.deepAccess(bid, 'mediaTypes.video.context')) { + return false; + } + + if (!utils.contains(ENVS, bid.params.env)) { + return false; + } + + return !!(bid.params.env && bid.params.pubId && bid.params.zoneId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests) { + return validBidRequests.map(bid => { + // We take the first size + let size = utils.parseSizesInput(bid.sizes)[0].split('x'); + + // Get the page's url + let referrer = utils.getTopWindowUrl(); + if (bid.params.referrer) { + referrer = bid.params.referrer; + } + + // Get Video Context + let context = utils.deepAccess(bid, 'mediaTypes.video.context'); + + let url = '//player.mediabong.' + bid.params.env + '/prebid/request'; + let data = { + width: size[0], + height: size[1], + pub_id: bid.params.pubId, + zone_id: bid.params.zoneId, + context: context, + floor_price: bid.params.floorPrice ? bid.params.floorPrice : 0, + url: referrer, + env: bid.params.env, + bid_id: bid.bidId + }; + + return { + method: 'POST', + url: url, + data: data + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bid) { + const responseBody = serverResponse.body; + + if (typeof responseBody !== 'object' || responseBody.status !== 'ok') { + return []; + } + + let responses = []; + let reponse = { + requestId: bid.data.bid_id, + cpm: responseBody.cpm, + width: bid.data.width, + height: bid.data.height, + ttl: TTL, + creativeId: responseBody.creativeId, + netRevenue: true, + currency: CURRENCIES[bid.data.env], + vastUrl: responseBody.url + }; + responses.push(reponse); + + return responses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses) { + if (syncOptions.iframeEnabled) { + if (serverResponses.length > 0) { + let responseBody = serverResponses[0].body; + if (typeof responseBody !== 'object' || responseBody.iframeSync) { + return [{ + type: 'iframe', + url: responseBody.iframeSync + }]; + } + } + } + return []; + } +}; + +registerBidder(spec); diff --git a/modules/vubleBidAdapter.md b/modules/vubleBidAdapter.md new file mode 100644 index 000000000000..4e066b9dd8aa --- /dev/null +++ b/modules/vubleBidAdapter.md @@ -0,0 +1,58 @@ +# Overview + +``` +Module Name: Vuble Bidder Adapter +Module Type: Vuble Adapter +Maintainer: gv@mediabong.com +``` + +# Description + +Module that connects to Vuble's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-video-instream', + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + bids: [ + { + bidder: "vuble", + params: { + env: 'net', + pubId: '3', + zoneId: '12345', + referrer: "http://www.vuble.tv/", // optional + floorPrice: 5.00 // optional + } + } + ] + }, + { + code: 'test-video-outstream', + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'outstream' + } + }, + bids: [ + { + bidder: "vuble", + params: { + env: 'net', + pubId: '3', + zoneId: '12345', + referrer: "http://www.vuble.tv/", // optional + floorPrice: 5.00 // optional + } + } + ] + } + ]; \ No newline at end of file diff --git a/test/spec/modules/vubleBidAdapter_spec.js b/test/spec/modules/vubleBidAdapter_spec.js new file mode 100644 index 000000000000..3ecbd533c5fc --- /dev/null +++ b/test/spec/modules/vubleBidAdapter_spec.js @@ -0,0 +1,268 @@ +// import or require modules necessary for the test, e.g.: + +import {expect} from 'chai'; +import {spec as adapter} from 'modules/vubleBidAdapter'; +import * as utils from 'src/utils'; + +describe('VubleAdapter', () => { + describe('Check methods existance', () => { + it('exists and is a function', () => { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + it('exists and is a function', () => { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + it('exists and is a function', () => { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + it('exists and is a function', () => { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + }); + + describe('Check method isBidRequestValid return', () => { + let bid = { + bidder: 'vuble', + params: { + env: 'net', + pubId: '3', + zoneId: '12345', + floorPrice: 5.00 // optional + }, + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + }; + + it('should be true', () => { + expect(adapter.isBidRequestValid(bid)).to.be.true; + }); + + it('should be false because the sizes are missing or in the wrong format', () => { + let wrongBid = utils.deepClone(bid); + wrongBid.sizes = '640360'; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + + wrongBid = utils.deepClone(bid); + delete wrongBid.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + }); + + it('should be false because the mediaType is missing or wrong', () => { + let wrongBid = utils.deepClone(bid); + wrongBid.mediaTypes = {}; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + + wrongBid = utils.deepClone(bid); + delete wrongBid.mediaTypes; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + }); + + it('should be false because the env is missing or wrong', () => { + let wrongBid = utils.deepClone(bid); + wrongBid.params.env = 'us'; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + + wrongBid = utils.deepClone(bid); + delete wrongBid.params.env; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + }); + + it('should be false because params.pubId is missing', () => { + let wrongBid = utils.deepClone(bid); + delete wrongBid.params.pubId; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + }); + + it('should be false because params.zoneId is missing', () => { + let wrongBid = utils.deepClone(bid); + delete wrongBid.params.zoneId; + expect(adapter.isBidRequestValid(wrongBid)).to.be.false; + }); + }); + + describe('Check buildRequests method', () => { + let sandbox; + before(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(utils, 'getTopWindowUrl').returns('http://www.vuble.tv/'); + }); + + // Bids to be formatted + let bid1 = { + bidder: 'vuble', + params: { + env: 'net', + pubId: '3', + zoneId: '12345', + floorPrice: 5.50 // optional + }, + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + bidId: 'abdc' + }; + let bid2 = { + bidder: 'vuble', + params: { + env: 'com', + pubId: '8', + zoneId: '2468', + referrer: 'http://www.vuble.fr/' + }, + sizes: '640x360', + mediaTypes: { + video: { + context: 'outstream' + } + }, + bidId: 'efgh' + }; + + // Formatted requets + let request1 = { + method: 'POST', + url: '//player.mediabong.net/prebid/request', + data: { + width: '640', + height: '360', + pub_id: '3', + zone_id: '12345', + context: 'instream', + floor_price: 5.5, + url: 'http://www.vuble.tv/', + env: 'net', + bid_id: 'abdc' + } + }; + let request2 = { + method: 'POST', + url: '//player.mediabong.com/prebid/request', + data: { + width: '640', + height: '360', + pub_id: '8', + zone_id: '2468', + context: 'outstream', + floor_price: 0, + url: 'http://www.vuble.fr/', + env: 'com', + bid_id: 'efgh' + } + }; + + it('must return the right formatted requests', () => { + let rs = adapter.buildRequests([bid1, bid2]); + expect(adapter.buildRequests([bid1, bid2])).to.deep.equal([request1, request2]); + }); + + after(() => { + sandbox.restore(); + }); + }); + + describe('Check interpretResponse method return', () => { + // Server's response + let response = { + body: { + status: 'ok', + cpm: 5.00, + creativeId: '2468', + url: 'https//player.mediabong.net/prebid/ad/a1b2c3d4' + } + }; + // bid Request + let bid = { + data: { + context: 'instream', + env: 'net', + width: '640', + height: '360', + pub_id: '3', + zone_id: '12345', + bid_id: 'abdc', + floor_price: 5.50 // optional + }, + method: 'POST', + url: '//player.mediabong.net/prebid/request' + }; + // Formatted reponse + let result = { + requestId: 'abdc', + cpm: 5.00, + width: '640', + height: '360', + ttl: 60, + creativeId: '2468', + netRevenue: true, + currency: 'USD', + vastUrl: 'https//player.mediabong.net/prebid/ad/a1b2c3d4' + }; + + it('should equal to the expected formatted result', () => { + expect(adapter.interpretResponse(response, bid)).to.deep.equal([result]); + }); + + it('should be empty because the status is missing or wrong', () => { + let wrongResponse = utils.deepClone(response); + wrongResponse.body.status = 'ko'; + expect(adapter.interpretResponse(wrongResponse, bid)).to.be.empty; + + wrongResponse = utils.deepClone(response); + delete wrongResponse.body.status; + expect(adapter.interpretResponse(wrongResponse, bid)).to.be.empty; + }); + + it('should be empty because the body is missing or wrong', () => { + let wrongResponse = utils.deepClone(response); + wrongResponse.body = [1, 2, 3]; + expect(adapter.interpretResponse(wrongResponse, bid)).to.be.empty; + + wrongResponse = utils.deepClone(response); + delete wrongResponse.body; + expect(adapter.interpretResponse(wrongResponse, bid)).to.be.empty; + }); + }); + + describe('Check getUserSyncs method return', () => { + // Sync options + let syncOptions = { + iframeEnabled: false + }; + // Server's response + let response = { + body: { + status: 'ok', + cpm: 5.00, + creativeId: '2468', + url: 'https//player.mediabong.net/prebid/ad/a1b2c3d4' + } + }; + // Formatted reponse + let result = { + type: 'iframe', + url: 'http://player.mediabong.net/csifr?1234' + }; + + it('should return an empty array', () => { + expect(adapter.getUserSyncs({}, [])).to.be.empty; + expect(adapter.getUserSyncs({}, [])).to.be.empty; + expect(adapter.getUserSyncs(syncOptions, [response])).to.be.empty; + expect(adapter.getUserSyncs(syncOptions, [response])).to.be.empty; + syncOptions.iframeEnabled = true; + expect(adapter.getUserSyncs(syncOptions, [response])).to.be.empty; + expect(adapter.getUserSyncs(syncOptions, [response])).to.be.empty; + }); + + it('should be equal to the expected result', () => { + response.body.iframeSync = 'http://player.mediabong.net/csifr?1234'; + expect(adapter.getUserSyncs(syncOptions, [response])).to.deep.equal([result]); + }) + }); +});