From 03c9e2c70e30ecad720d765051dc10efabaa42bd Mon Sep 17 00:00:00 2001 From: Erik Chau Date: Wed, 22 Feb 2017 13:49:38 -0800 Subject: [PATCH 1/3] Add TapSense Header Bidding Adapter and tests --- adapters.json | 1 + src/adapters/tapsense.js | 89 +++++++++++ test/spec/adapters/tapsense_spec.js | 237 ++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 src/adapters/tapsense.js create mode 100644 test/spec/adapters/tapsense_spec.js diff --git a/adapters.json b/adapters.json index 3aa09851a33..15293c19af2 100644 --- a/adapters.json +++ b/adapters.json @@ -52,6 +52,7 @@ "vertoz", "widespace", "admixer", + "tapsense", { "appnexus": { "alias": "brealtime" diff --git a/src/adapters/tapsense.js b/src/adapters/tapsense.js new file mode 100644 index 00000000000..ee3b27665ea --- /dev/null +++ b/src/adapters/tapsense.js @@ -0,0 +1,89 @@ +//v0.0.1 +var bidfactory = require('../bidfactory.js'); +var bidmanager = require('../bidmanager.js'); +var adloader = require('../adloader'); +var utils = require('../utils.js'); + +var TapSenseAdapter = function TapSenseAdapter() { + var version = "0.0.1"; + var creativeSizes = [ + "320x50" + ]; + var validParams = [ + "ufid", + "refer", + "ad_unit_id", + "device_id", + "lat", + "long", + "user", + "price_floor", + "test", + "jsonp" + ]; + var bids; + window.tapsense = {}; + function _callBids(params) { + bids = params.bids || []; + for (var i = 0; i < bids.length; i++) { + var bid = bids[i]; + var isValidSize = false; + if (!bid.sizes) { + return; + } + for (var k = 0; k < bid.sizes.length; k++) { + if (creativeSizes.indexOf(bid.sizes[k].join("x")) > -1) { + isValidSize = true; + break; + } + } + if (isValidSize) { + if (!bid.params.scriptURL) { + continue; + } + var queryString = "?price=true&callback=tapsense.callback_with_price_" + bid.bidId + "&version=" + version + "&"; + window.tapsense["callback_with_price_" + bid.bidId] = generateCallback(bid.bidId); + var keys = Object.keys(bid.params); + for (var j = 0; j < keys.length; j++) { + if (validParams.indexOf(keys[j]) < 0) continue; + queryString += encodeURIComponent(keys[j]) + "=" + encodeURIComponent(bid.params[keys[j]]) + "&"; + } + var scriptURL = bid.params.scriptURL; + _requestBids(scriptURL + queryString); + } + } + } + + function generateCallback(bidId){ + return function(response, price) { + var bidObj; + if (response && price) { + var bidReq = utils.getBidRequest(bidId); + if (response.status.value === "ok" && response.count_ad_units > 0) { + bidObj = bidfactory.createBid(1, bidObj); + bidObj.cpm = price; + bidObj.width = response.width; + bidObj.height = response.height; + bidObj.ad = response.ad_units[0].html; + } else { + bidObj = bidfactory.createBid(2, bidObj); + } + bidObj.bidderCode = bidReq.bidder; + bidmanager.addBidResponse(bidReq.placementCode, bidObj); + + } else { + utils.logMessage('No prebid response'); + } + }; + } + + function _requestBids(scriptURL) { + adloader.loadScript(scriptURL); + } + + return { + callBids: _callBids + }; +}; + +module.exports = TapSenseAdapter; diff --git a/test/spec/adapters/tapsense_spec.js b/test/spec/adapters/tapsense_spec.js new file mode 100644 index 00000000000..7725100dd00 --- /dev/null +++ b/test/spec/adapters/tapsense_spec.js @@ -0,0 +1,237 @@ +import { expect } from 'chai'; +import Adapter from 'src/adapters/tapsense'; +import bidmanager from 'src/bidmanager'; +import adloader from "src/adloader"; +import * as utils from "src/utils"; + +const DEFAULT_BIDDER_REQUEST = { + "bidderCode": "tapsense", + "bidderRequestId": "141ed07a281ca3", + "requestId": "b202e550-b0f7-4fb9-bfb4-1aa80f1795b4", + "start": new Date().getTime(), + "bids": [ + { + "sizes": undefined, //set values in tests + "bidder": "tapsense", + "bidId": "2b211418dd0575", + "bidderRequestId": "141ed07a281ca3", + "placementCode": "thisisatest", + "params": { + "ufid": "thisisaufid", + "refer": "thisisarefer", + "version": "0.0.1", + "jsonp": 1, + "ad_unit_id": "thisisanadunitid", + "device_id": "thisisadeviceid", + "lat": "thisislat", + "long": "thisisalong", + "user": "thisisanidfa", + "price_floor": 0.01 + } + } + ] +} + +const SUCCESSFUL_RESPONSE = { + "count_ad_units": 1, + "status": { + "value": "ok", + }, + "ad_units": [ + { + html: "", + imp_url: "https://i.tapsense.com" + } + ], + "id": "thisisanid", + "width": 320, + "height": 50, + "time": new Date().getTime() +} + +const UNSUCCESSFUL_RESPONSE = { + "count_ad_units": 0, + "status": { + "value": "nofill" //will be set in test + }, + "time": new Date().getTime() +} + +function duplicate(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +function makeSuccessfulRequest(adapter){ + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = [[320,50], [500,500]]; + modifiedReq.bids[0].params.scriptURL = "https://ads.tapsense.com"; + adapter.callBids(modifiedReq); + return modifiedReq.bids; +} + +describe ("TapSenseAdapter", () => { + let adapter, sandbox; + + beforeEach(() => { + adapter = new Adapter; + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }) + + describe('request function', () => { + + beforeEach(() => { + sandbox.stub(adloader, 'loadScript'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + it('requires parameters to make request', () => { + adapter.callBids({}); + sinon.assert.notCalled(adloader.loadScript); + }); + + it('does not make a request if missing sizes', () => { + adapter.callBids(DEFAULT_BIDDER_REQUEST); + sinon.assert.notCalled(adloader.loadScript); + }); + + it('does not make a request if ad sizes are invalid/incorrect', () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = [[500,500]]; + adapter.callBids(modifiedReq); + sinon.assert.notCalled(adloader.loadScript); + }); + + it('does not make a request if no scriptURL is provided in bid params', () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = [[320,50]]; + adapter.callBids(modifiedReq); + sinon.assert.notCalled(adloader.loadScript); + }) + + describe("requesting an ad", () => { + beforeEach(() => { + makeSuccessfulRequest(adapter); + }); + afterEach(() => { + sandbox.restore(); + }) + it("makes a request if both valid sizes and scriptURL are provided", () => { + sinon.assert.calledOnce(adloader.loadScript); + expect(adloader.loadScript.firstCall.args[0]).to.contain( + "ads.tapsense.com" + ); + }); + it("appends bid params as a query string when requesting ad", () => { + sinon.assert.calledOnce(adloader.loadScript); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /ufid=thisisaufid&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /refer=thisisarefer&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /version=[^&]+&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /jsonp=1&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /ad_unit_id=thisisanadunitid&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /device_id=thisisadeviceid&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /lat=thisislat&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /long=thisisalong&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /user=thisisanidfa&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /price_floor=0\.01&/ + ); + expect(adloader.loadScript.firstCall.args[0]).to.not.contain( + "scriptUrl" + ); + }) + }) + }); + + describe("generateCallback", () => { + beforeEach(() => { + sandbox.stub(adloader, 'loadScript'); + }); + afterEach(() => { + sandbox.restore(); + }); + it("generates callback in namespaced object with correct bidder id", () => { + makeSuccessfulRequest(adapter); + expect(tapsense.callback_with_price_2b211418dd0575).to.exist.and.to.be.a('function'); + }) + }); + + describe("response", () => { + beforeEach(() => { + sandbox.stub(bidmanager, 'addBidResponse'); + sandbox.stub(adloader, 'loadScript'); + let bids = makeSuccessfulRequest(adapter); + sandbox.stub(utils, "getBidRequest", (id) => { + return bids.find((item) => { return item.bidId === id}); + }) + }); + afterEach(() => { + sandbox.restore(); + }); + describe("successful response", () => { + beforeEach(() => { + tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE, 1.2); + }); + it("called the bidmanager and registers a bid", () => { + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(1); + }); + it("should have the correct placementCode", () => { + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal("thisisatest"); + }); + }); + describe("unsuccessful response", () => { + beforeEach(() => { + tapsense.callback_with_price_2b211418dd0575(UNSUCCESSFUL_RESPONSE, 1.2); + }) + it("should call the bidmanger and register an invalid bid", () => { + sinon.assert.calledOnce(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); + }); + it("should have the correct placementCode", () => { + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal("thisisatest"); + }) + }); + describe("no response/timeout", () => { + it("should not register any bids", () => { + sinon.assert.notCalled(bidmanager.addBidResponse); + }) + }); + describe("edge cases", () => { + it("does not register a bid if no price is supplied", () => { + sandbox.stub(utils, "logMessage"); + tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE); + sinon.assert.notCalled(bidmanager.addBidResponse); + }); + }); + }); + +}) From 429b0590229c5270420b86d9f533da9f8539b3cf Mon Sep 17 00:00:00 2001 From: Erik Chau Date: Mon, 6 Mar 2017 16:58:57 -0800 Subject: [PATCH 2/3] Update for Tapsense Prebid Header * changed var to es6 let/const * when checking for bid sizes, use utils.parseSizesInput to handle single/nested arrays * use template strings where applicable * use $$PREBID_GLOBAL$$ instead of window * scriptUrl is now static * named anonymous function in generateCallBack * add more tests in tapsense_spec.js --- src/adapters/tapsense.js | 62 ++++++++++++----------- test/spec/adapters/tapsense_spec.js | 76 ++++++++++++++++++----------- 2 files changed, 78 insertions(+), 60 deletions(-) diff --git a/src/adapters/tapsense.js b/src/adapters/tapsense.js index ee3b27665ea..5c3bafab255 100644 --- a/src/adapters/tapsense.js +++ b/src/adapters/tapsense.js @@ -1,64 +1,62 @@ //v0.0.1 -var bidfactory = require('../bidfactory.js'); -var bidmanager = require('../bidmanager.js'); -var adloader = require('../adloader'); -var utils = require('../utils.js'); -var TapSenseAdapter = function TapSenseAdapter() { - var version = "0.0.1"; - var creativeSizes = [ +const bidfactory = require('../bidfactory.js'); +const bidmanager = require('../bidmanager.js'); +const adloader = require('../adloader'); +const utils = require('../utils.js'); + +const TapSenseAdapter = function TapSenseAdapter() { + const version = "0.0.1"; + const creativeSizes = [ "320x50" ]; - var validParams = [ + const validParams = [ "ufid", "refer", - "ad_unit_id", + "ad_unit_id", //required "device_id", "lat", "long", - "user", + "user", //required "price_floor", - "test", - "jsonp" + "test" ]; - var bids; - window.tapsense = {}; + const SCRIPT_URL = "https://ads04.tapsense.com/ads/headerad"; + let bids; + $$PREBID_GLOBAL$$.tapsense = {}; function _callBids(params) { bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - var isValidSize = false; - if (!bid.sizes) { + for (let i = 0; i < bids.length; i++) { + let bid = bids[i]; + let isValidSize = false; + if (!bid.sizes || !bid.params.user || !bid.params.ad_unit_id) { return; } - for (var k = 0; k < bid.sizes.length; k++) { - if (creativeSizes.indexOf(bid.sizes[k].join("x")) > -1) { + let parsedSizes = utils.parseSizesInput(bid.sizes); + for (let k = 0; k < parsedSizes.length; k++) { + if (creativeSizes.indexOf(parsedSizes[k]) > -1) { isValidSize = true; break; } } if (isValidSize) { - if (!bid.params.scriptURL) { - continue; - } - var queryString = "?price=true&callback=tapsense.callback_with_price_" + bid.bidId + "&version=" + version + "&"; - window.tapsense["callback_with_price_" + bid.bidId] = generateCallback(bid.bidId); - var keys = Object.keys(bid.params); - for (var j = 0; j < keys.length; j++) { + let queryString = `?price=true&jsonp=1&callback=tapsense.callback_with_price_${bid.bidId}&version=${version}&`; + $$PREBID_GLOBAL$$.tapsense[`callback_with_price_${bid.bidId}`] = generateCallback(bid.bidId); + let keys = Object.keys(bid.params); + for (let j = 0; j < keys.length; j++) { if (validParams.indexOf(keys[j]) < 0) continue; queryString += encodeURIComponent(keys[j]) + "=" + encodeURIComponent(bid.params[keys[j]]) + "&"; } - var scriptURL = bid.params.scriptURL; - _requestBids(scriptURL + queryString); + _requestBids(SCRIPT_URL + queryString); } } } function generateCallback(bidId){ - return function(response, price) { - var bidObj; + return function tapsenseCallback(response, price) { + let bidObj; if (response && price) { - var bidReq = utils.getBidRequest(bidId); + let bidReq = utils.getBidRequest(bidId); if (response.status.value === "ok" && response.count_ad_units > 0) { bidObj = bidfactory.createBid(1, bidObj); bidObj.cpm = price; diff --git a/test/spec/adapters/tapsense_spec.js b/test/spec/adapters/tapsense_spec.js index 7725100dd00..7bd5a475245 100644 --- a/test/spec/adapters/tapsense_spec.js +++ b/test/spec/adapters/tapsense_spec.js @@ -4,6 +4,8 @@ import bidmanager from 'src/bidmanager'; import adloader from "src/adloader"; import * as utils from "src/utils"; +window.pbjs = window.pbjs || {}; + const DEFAULT_BIDDER_REQUEST = { "bidderCode": "tapsense", "bidderRequestId": "141ed07a281ca3", @@ -20,7 +22,6 @@ const DEFAULT_BIDDER_REQUEST = { "ufid": "thisisaufid", "refer": "thisisarefer", "version": "0.0.1", - "jsonp": 1, "ad_unit_id": "thisisanadunitid", "device_id": "thisisadeviceid", "lat": "thisislat", @@ -64,14 +65,12 @@ function duplicate(obj) { function makeSuccessfulRequest(adapter){ let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); modifiedReq.bids[0].sizes = [[320,50], [500,500]]; - modifiedReq.bids[0].params.scriptURL = "https://ads.tapsense.com"; adapter.callBids(modifiedReq); return modifiedReq.bids; } describe ("TapSenseAdapter", () => { let adapter, sandbox; - beforeEach(() => { adapter = new Adapter; sandbox = sinon.sandbox.create(); @@ -81,57 +80,81 @@ describe ("TapSenseAdapter", () => { }) describe('request function', () => { - beforeEach(() => { sandbox.stub(adloader, 'loadScript'); }); - afterEach(() => { sandbox.restore(); }); - it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); - it('requires parameters to make request', () => { adapter.callBids({}); sinon.assert.notCalled(adloader.loadScript); }); - - it('does not make a request if missing sizes', () => { - adapter.callBids(DEFAULT_BIDDER_REQUEST); + it('does not make a request if missing user', () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + delete modifiedReq.bids.user + adapter.callBids(modifiedReq); sinon.assert.notCalled(adloader.loadScript); }); - - it('does not make a request if ad sizes are invalid/incorrect', () => { + it('does not make a request if missing ad_unit_id', () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + delete modifiedReq.bids.ad_unit_id + adapter.callBids(modifiedReq); + sinon.assert.notCalled(adloader.loadScript); + }); + it('does not make a request if ad sizes are incorrect', () => { let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); modifiedReq.bids[0].sizes = [[500,500]]; adapter.callBids(modifiedReq); sinon.assert.notCalled(adloader.loadScript); }); - - it('does not make a request if no scriptURL is provided in bid params', () => { + it('does not make a request if ad sizes are invalid format', () => { let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); - modifiedReq.bids[0].sizes = [[320,50]]; + modifiedReq.bids[0].sizes = 1234; adapter.callBids(modifiedReq); sinon.assert.notCalled(adloader.loadScript); - }) + }); describe("requesting an ad", () => { - beforeEach(() => { - makeSuccessfulRequest(adapter); - }); afterEach(() => { sandbox.restore(); }) - it("makes a request if both valid sizes and scriptURL are provided", () => { + it("makes a request if valid sizes are provided (nested array)", () => { + makeSuccessfulRequest(adapter); sinon.assert.calledOnce(adloader.loadScript); expect(adloader.loadScript.firstCall.args[0]).to.contain( - "ads.tapsense.com" + "ads04.tapsense.com" + ); + }); + it("handles a singles array for size parameter", () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = [320,50]; + adapter.callBids(modifiedReq); + expect(adloader.loadScript.firstCall.args[0]).to.contain( + "ads04.tapsense.com" + ); + }); + it("handles a string for size parameter", () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = "320x50"; + adapter.callBids(modifiedReq); + expect(adloader.loadScript.firstCall.args[0]).to.contain( + "ads04.tapsense.com" + ); + }); + it("handles a string with multiple sizes for size parameter", () => { + let modifiedReq = duplicate(DEFAULT_BIDDER_REQUEST); + modifiedReq.bids[0].sizes = "320x50,500x500"; + adapter.callBids(modifiedReq); + expect(adloader.loadScript.firstCall.args[0]).to.contain( + "ads04.tapsense.com" ); }); it("appends bid params as a query string when requesting ad", () => { + makeSuccessfulRequest(adapter); sinon.assert.calledOnce(adloader.loadScript); expect(adloader.loadScript.firstCall.args[0]).to.match( /ufid=thisisaufid&/ @@ -163,9 +186,6 @@ describe ("TapSenseAdapter", () => { expect(adloader.loadScript.firstCall.args[0]).to.match( /price_floor=0\.01&/ ); - expect(adloader.loadScript.firstCall.args[0]).to.not.contain( - "scriptUrl" - ); }) }) }); @@ -179,7 +199,7 @@ describe ("TapSenseAdapter", () => { }); it("generates callback in namespaced object with correct bidder id", () => { makeSuccessfulRequest(adapter); - expect(tapsense.callback_with_price_2b211418dd0575).to.exist.and.to.be.a('function'); + expect(pbjs.tapsense.callback_with_price_2b211418dd0575).to.exist.and.to.be.a('function'); }) }); @@ -197,7 +217,7 @@ describe ("TapSenseAdapter", () => { }); describe("successful response", () => { beforeEach(() => { - tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE, 1.2); + pbjs.tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE, 1.2); }); it("called the bidmanager and registers a bid", () => { sinon.assert.calledOnce(bidmanager.addBidResponse); @@ -210,7 +230,7 @@ describe ("TapSenseAdapter", () => { }); describe("unsuccessful response", () => { beforeEach(() => { - tapsense.callback_with_price_2b211418dd0575(UNSUCCESSFUL_RESPONSE, 1.2); + pbjs.tapsense.callback_with_price_2b211418dd0575(UNSUCCESSFUL_RESPONSE, 1.2); }) it("should call the bidmanger and register an invalid bid", () => { sinon.assert.calledOnce(bidmanager.addBidResponse); @@ -228,7 +248,7 @@ describe ("TapSenseAdapter", () => { describe("edge cases", () => { it("does not register a bid if no price is supplied", () => { sandbox.stub(utils, "logMessage"); - tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE); + pbjs.tapsense.callback_with_price_2b211418dd0575(SUCCESSFUL_RESPONSE); sinon.assert.notCalled(bidmanager.addBidResponse); }); }); From 01b2df877cd352c45ff1546c999d916c58f5f00a Mon Sep 17 00:00:00 2001 From: Erik Chau Date: Mon, 6 Mar 2017 17:36:45 -0800 Subject: [PATCH 3/3] Url Callback parameter needed prebid global object --- src/adapters/tapsense.js | 2 +- test/spec/adapters/tapsense_spec.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/adapters/tapsense.js b/src/adapters/tapsense.js index 5c3bafab255..f656c580546 100644 --- a/src/adapters/tapsense.js +++ b/src/adapters/tapsense.js @@ -40,7 +40,7 @@ const TapSenseAdapter = function TapSenseAdapter() { } } if (isValidSize) { - let queryString = `?price=true&jsonp=1&callback=tapsense.callback_with_price_${bid.bidId}&version=${version}&`; + let queryString = `?price=true&jsonp=1&callback=$$PREBID_GLOBAL$$.tapsense.callback_with_price_${bid.bidId}&version=${version}&`; $$PREBID_GLOBAL$$.tapsense[`callback_with_price_${bid.bidId}`] = generateCallback(bid.bidId); let keys = Object.keys(bid.params); for (let j = 0; j < keys.length; j++) { diff --git a/test/spec/adapters/tapsense_spec.js b/test/spec/adapters/tapsense_spec.js index 7bd5a475245..72d189ef81e 100644 --- a/test/spec/adapters/tapsense_spec.js +++ b/test/spec/adapters/tapsense_spec.js @@ -186,6 +186,9 @@ describe ("TapSenseAdapter", () => { expect(adloader.loadScript.firstCall.args[0]).to.match( /price_floor=0\.01&/ ); + expect(adloader.loadScript.firstCall.args[0]).to.match( + /callback=pbjs\.tapsense\.callback_with_price_.+&/ + ); }) }) });