diff --git a/modules/mobfoxBidAdapter.js b/modules/mobfoxBidAdapter.js index 39d2fe40f1d..ff55d330112 100644 --- a/modules/mobfoxBidAdapter.js +++ b/modules/mobfoxBidAdapter.js @@ -1,34 +1,25 @@ -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const ajax = require('src/ajax.js'); -const CONSTANTS = require('src/constants.json'); +import {registerBidder} from 'src/adapters/bidderFactory'; + const utils = require('src/utils.js'); -const adaptermanager = require('src/adaptermanager'); - -function MobfoxAdapter() { - const BIDDER_CODE = 'mobfox'; - const BID_REQUEST_BASE_URL = 'https://my.mobfox.com/request.php'; - - // request - function buildQueryStringFromParams(params) { - for (let key in params) { - if (params.hasOwnProperty(key)) { - if (params[key] === undefined) { - delete params[key]; - } else { - params[key] = encodeURIComponent(params[key]); - } - } +const BIDDER_CODE = 'mobfox'; +const BID_REQUEST_BASE_URL = 'https://my.mobfox.com/request.php'; +const CPM_HEADER = 'X-Pricing-CPM'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['mf'], // short code + isBidRequestValid: function (bid) { + return bid.params.s !== null && bid.params.s !== undefined && bid.requestId !== null && bid.requestId !== undefined; + }, + buildRequests: function (validBidRequests) { + if (validBidRequests.length > 1) { + throw ('invalid number of valid bid requests, expected 1 element') } - return utils._map(Object.keys(params), key => `${key}=${params[key]}`) - .join('&'); - } - - function buildBidRequest(bid) { - let bidParams = bid.params; + let bidParams = validBidRequests[0].params; + let bid = validBidRequests[0]; - let requestParams = { + let params = { // -------------------- Mandatory Parameters ------------------ rt: bidParams.rt || 'api-fetchip', r_type: bidParams.r_type || 'banner', @@ -80,103 +71,63 @@ function MobfoxAdapter() { n_rating_req: bidParams.n_rating_req || undefined }; - return requestParams; - } + let payloadString = buildPayloadString(params); - function sendBidRequest(bid) { - let requestParams = buildBidRequest(bid); - let queryString = buildQueryStringFromParams(requestParams); - - ajax.ajax(`${BID_REQUEST_BASE_URL}?${queryString}`, { - success(resp, xhr) { - if (xhr.getResponseHeader('Content-Type') == 'application/json') { - try { - resp = JSON.parse(resp) - } catch (e) { - resp = {error: resp} - } - } - onBidResponse({ - data: resp, - xhr: xhr - }, bid); - }, - error(err) { - if (xhr.getResponseHeader('Content-Type') == 'application/json') { - try { - err = JSON.parse(err); - } catch (e) { - } - ; - } - onBidResponseError(bid, [err]); + return { + method: 'GET', + url: BID_REQUEST_BASE_URL, + data: payloadString, + requestId: bid.bidId + }; + }, + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + let serverResponseBody = serverResponse.body; + + if (!serverResponseBody || serverResponseBody.error) { + let errorMessage = `in response for ${BIDDER_CODE} adapter`; + if (serverResponseBody && serverResponseBody.error) { + errorMessage += `: ${serverResponseBody.error}`; } - }); - } - - // response - function onBidResponseError(bid, err) { - utils.logError('Bid Response Error', bid, ...err); - let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bid); - bidResponse.bidderCode = BIDDER_CODE; - bidmanager.addBidResponse(bid.placementCode, bidResponse); - } - - function onBidResponse(bidderResponse, bid) { - // transform the response to a valid prebid response - try { - let bidResponse = transformResponse(bidderResponse, bid); - bidmanager.addBidResponse(bid.placementCode, bidResponse); - } catch (e) { - onBidResponseError(bid, [e]); - } - } - - function transformResponse(bidderResponse, bid) { - let responseBody = bidderResponse.data; - - // Validate Request - let err = responseBody.error; - if (err) { - throw err; + utils.logError(errorMessage); + return bidResponses; } - - let htmlString = responseBody.request && responseBody.request.htmlString; - if (!htmlString) { - throw [`htmlString is missing`, responseBody]; - } - - let cpm; - const cpmHeader = bidderResponse.xhr.getResponseHeader('X-Pricing-CPM'); try { - cpm = Number(cpmHeader); + let serverResponseHeaders = serverResponse.headers; + let bidRequestData = bidRequest.data.split('&'); + const bidResponse = { + requestId: bidRequest.requestId, + cpm: serverResponseHeaders.get(CPM_HEADER), + width: bidRequestData[5].split('=')[1], + height: bidRequestData[6].split('=')[1], + creativeId: bidRequestData[3].split('=')[1], + currency: 'USD', + netRevenue: true, + ttl: 360, + referrer: serverResponseBody.request.clickurl, + ad: serverResponseBody.request.htmlString + }; + bidResponses.push(bidResponse); } catch (e) { - throw ['Invalid CPM value:', cpmHeader]; + throw 'could not build bid response: ' + e; } - - // Validations passed - Got bid - let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bid); - bidResponse.bidderCode = BIDDER_CODE; - - bidResponse.ad = htmlString; - bidResponse.cpm = cpm; - - bidResponse.width = bid.sizes[0][0]; - bidResponse.height = bid.sizes[0][1]; - - return bidResponse; + return bidResponses; } - - // prebid api - function callBids(params) { - let bids = params.bids || []; - bids.forEach(sendBidRequest); +}; + +function buildPayloadString(params) { + for (let key in params) { + if (params.hasOwnProperty(key)) { + if (params[key] === undefined) { + delete params[key]; + } else { + params[key] = encodeURIComponent(params[key]); + } + } } - return { - callBids: callBids - }; + return utils._map(Object.keys(params), key => `${key}=${params[key]}`) + .join('&') } -adaptermanager.registerBidAdapter(new MobfoxAdapter(), 'mobfox'); -module.exports = MobfoxAdapter; +registerBidder(spec); diff --git a/modules/mobfoxBidAdapter.md b/modules/mobfoxBidAdapter.md new file mode 100644 index 00000000000..31b60606d2f --- /dev/null +++ b/modules/mobfoxBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +``` +Module Name: Mobfox Bidder Adapter +Module Type: Bidder Adapter +Maintainer: solutions-team@matomy.com +``` + +# Description + +Module that connects to Mobfox's demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300,600]], + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'mobfox', + params: { + s: "267d72ac3f77a3f447b32cf7ebf20673", // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + } + }] + + }]; +``` diff --git a/test/spec/modules/mobfoxBidAdapter_spec.js b/test/spec/modules/mobfoxBidAdapter_spec.js index f2819009908..76bb95430fe 100644 --- a/test/spec/modules/mobfoxBidAdapter_spec.js +++ b/test/spec/modules/mobfoxBidAdapter_spec.js @@ -1,162 +1,142 @@ -describe('mobfox adapter tests', function () { +describe('mobfox adapter tests', () => { const expect = require('chai').expect; const utils = require('src/utils'); const adapter = require('modules/mobfoxBidAdapter'); - const bidmanager = require('src/bidmanager'); - const adloader = require('src/adloader'); - const CONSTANTS = require('src/constants.json'); - const ajax = require('src/ajax.js'); - let mockResponses = { - banner: { - 'request': { - 'type': 'textAd', - 'htmlString': '<\/title><style>body{margin:0;padding:0}#mobfoxCover{background:0 0;margin:0;padding:0;border:none;position:absolute;left:0;top:0;z-index:100}<\/style><\/head><body><div id="mobfoxCover"><\/div><script type="text\/javascript">function checkRedirect(e){return function(){if(state===REDIRECT){state=REDUNDANT;var t=window.document.querySelector("iframe").contentDocument.querySelector("html").innerHTML.toLowerCase();if(!(t.indexOf("<script")<0&&t.indexOf("<iframe")<0)){var o=new XMLHttpRequest,d={creativeId:creativeId,advertiserId:advertiserId,hParam:hParam,dspId:dspId,networkId:networkId,autoPilotInventoryConfId:autoPilotInventoryConfId,stackItemId:stackItemId,adSpaceId:adSpaceId,cId:cId,adomain:adomain,geo:geo,event:e,ua:window.navigator.userAgent,adId:adId,site:window.location.href,md5Hash:md5Hash,snapshot:btoa(unescape(encodeURIComponent(t)))};o.open("POST","http:\/\/my.mobfox.com\/fraud-integration",!1),o.setRequestHeader("Content-type","application\/json"),o.send(JSON.stringify(d))}}}}function init(){window.onbeforeunload=checkRedirect("onbeforeunload"),window.addEventListener("beforeunload",checkRedirect("beforeunload")),window.addEventListener("unload",checkRedirect("unload")),document.addEventListener("visibilitychange",function(){"hidden"===document.visibilityState&&checkRedirect("visibilityState")});var e=document.createElement("iframe");document.body.appendChild(e),e.width="320",e.height="50";var t=document.querySelector("#mobfoxCover");t.style.width=e.width+"px",t.style.height=e.height+"px",e.style.margin="0px",e.style.padding="0px",e.style.border="none",e.scrolling="no",e.style.overflow="hidden",e.sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation allow-same-origin";var o=atob(markupB64);setTimeout(function(){state=NORMAL},200),setTimeout(function(){var e=document.querySelector("#mobfoxCover");document.body.removeChild(e)},200);var d="srcdoc"in e,n=o;o.indexOf("<body>")<0&&(n="<html><body style="margin:0">"+o+"<\/body><\/html>"),d?e.srcdoc=n:(e.contentWindow.document.open(),e.contentWindow.document.write(n),e.contentWindow.document.close())}var markupB64="PGEgaHJlZj0iaHR0cDovL3Rva3lvLW15Lm1vYmZveC5jb20vZXhjaGFuZ2UuY2xpY2sucGhwP2g9ZGI1ZjZkOTJiMDk1OGI0ZDFlNjU4ZjZlNWRkNWY0MmUiIHRhcmdldD0iX2JsYW5rIj48aW1nIHNyYz0iaHR0cHM6Ly9jcmVhdGl2ZWNkbi5tb2Jmb3guY29tL2U4ZTkxNWYzMmJhOTVkM2JmMzY4YTM5N2EyMzQ4NzVmLmdpZiIgYm9yZGVyPSIwIi8+PC9hPjxicj48aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlIi8+PHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPmRvY3VtZW50LndyaXRlKCc8aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlJnRlc3Q9MSIvPicpOzwvc2NyaXB0Pg==",INITIAL=0,REDIRECT=1,REDUNDANT=2,NORMAL=3,state=INITIAL,creativeId="",advertiserId="",hParam="db5f6d92b0958b4d1e658f6e5dd5f42e",dspId="",networkId="",autoPilotInventoryConfId="",stackItemId="392746",serverHost="184.172.209.50",adSpaceId="",adId="",cId="",adomain="",geo="US",md5Hash="f3bd183c0b19faf12c76e75461cb8cac";document.addEventListener("DOMContentLoaded",function(e){state=REDIRECT}),setTimeout(init,1)<\/script><\/body><\/html>', - 'clicktype': 'safari', - 'clickurl': 'http://tokyo-my.mobfox.com/exchange.click.php?h=db5f6d92b0958b4d1e658f6e5dd5f42e', - 'urltype': 'link', - 'refresh': '30', - 'scale': 'no', - 'skippreflight': 'yes' - } - } - }; - - let mockRequestsParams = { - banner: { - rt: 'api', - r_type: 'banner', - i: '69.197.148.18', - s: 'fe96717d9875b9da4339ea5367eff1ec', - u: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4', - adspace_strict: 0, - - // o_iosadvid: '1976F519-26D0-4428-9891-3133253A453F', - // r_floor: '0.2', - // longitude: '12.12', - // latitude: '280.12', - // demo_gender: 'male', - // demo_age: '1982', - // demo_keywords: 'sports', - // adspace_width: 320, - // adspace_height: 50 - } - }; - before(() => sinon.stub(document.body, 'appendChild')); - after(() => document.body.appendChild.restore()); - - let xhrMock = { - getResponseHeader: getResponseHeaderMock - }; - function getResponseHeaderMock(header) { - switch (header) { - case 'Content-Type': - return 'application/json'; - case 'X-Pricing-CPM': - return '1'; - } - } - function createMobfoxErrorStub() { - return sinon.stub(ajax, 'ajax', (url, callbacks) => { - callbacks.success( - JSON.stringify({error: 'No Ad Available'}), - xhrMock - ); + const bidRequest = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + s: '267d72ac3f77a3f447b32cf7ebf20673', // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + requestId: 'c241c810-18d9-4aa4-a62f-8c1980d8d36b', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + describe('validRequests', () => { + let bidRequestInvalid1 = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + requestId: 'c241c810-18d9-4aa4-a62f-8c1980d8d36b', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + let bidRequestInvalid2 = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + s: '267d72ac3f77a3f447b32cf7ebf20673', // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + it('test valid MF request success', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequest[0]); + expect(isValid).to.equal(true); }); - } - function createMobfoxSuccessStub() { - return sinon.stub(ajax, 'ajax', (url, callbacks) => { - callbacks.success( - JSON.stringify(mockResponses.banner) - , xhrMock - ); + it('test valid MF request failed1', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequestInvalid1[0]); + expect(isValid).to.equal(false); }); - } - describe('test mobfox error response', function () { - let stubAddBidResponse, stubAjax; - before(function () { - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - stubAjax = createMobfoxErrorStub() + it('test valid MF request failed2', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequestInvalid2[0]); + expect(isValid).to.equal(false); }); - - after(function () { - stubAddBidResponse.restore(); - stubAjax.restore(); + }) + + describe('buildRequests', () => { + it('test build MF request', () => { + let request = adapter.spec.buildRequests(bidRequest); + let payload = request.data.split('&'); + expect(payload[0]).to.equal('rt=api-fetchip'); + expect(payload[1]).to.equal('r_type=banner'); + expect(payload[2]).to.equal('r_resp=json'); + expect(payload[3]).to.equal('s=267d72ac3f77a3f447b32cf7ebf20673'); + expect(payload[5]).to.equal('adspace_width=320'); + expect(payload[6]).to.equal('adspace_height=480'); + expect(payload[7]).to.equal('imp_instl=1'); }); - it('should add empty bid responses if no bids returned', function () { - let bidderRequest = { - bidderCode: 'mobfox', - bids: [ - { - bidId: 'bidId1', - bidder: 'mobfox', - params: {}, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] - }; - - // empty ads in bidresponse - let requestParams = utils.cloneJson(mockRequestsParams.banner); - requestParams.adspace_width = 1231564; // should return an error - bidderRequest.bids[0].params = requestParams; - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter().callBids(bidderRequest); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidResponse1.bidderCode).to.equal('mobfox'); + it('test build MF request', () => { + let request = adapter.spec.buildRequests(bidRequest); + let payload = request.data.split('&'); + expect(payload[0]).to.equal('rt=api-fetchip'); + expect(payload[1]).to.equal('r_type=banner'); + expect(payload[2]).to.equal('r_resp=json'); + expect(payload[3]).to.equal('s=267d72ac3f77a3f447b32cf7ebf20673'); + expect(payload[5]).to.equal('adspace_width=320'); + expect(payload[6]).to.equal('adspace_height=480'); + expect(payload[7]).to.equal('imp_instl=1'); }); - }); - - describe('test mobfox success response', function () { - let stubAddBidResponse, stubAjax; - before(function () { - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - stubAjax = createMobfoxSuccessStub() - }); - - after(function () { - stubAddBidResponse.restore(); - stubAjax.restore(); + }) + + describe('interceptResponse', () => { + let mockServerResponse = { + body: { + request: { + clicktype: 'safari', + clickurl: 'http://tokyo-my.mobfox.com/exchange.click.php?h=494ef76d5b0287a8b5ac8724855cb5e0', + cpmPrice: 50, + htmlString: 'test', + refresh: '30', + scale: 'no', + skippreflight: 'yes', + type: 'textAd', + urltype: 'link' + } + }, + headers: { + get: function (header) { + if (header === 'X-Pricing-CPM') { + return 50; + } + } + } + }; + it('test intercept response', () => { + let request = adapter.spec.buildRequests(bidRequest); + let bidResponses = adapter.spec.interpretResponse(mockServerResponse, request); + expect(bidResponses.length).to.equal(1); + expect(bidResponses[0].ad).to.equal('test'); + expect(bidResponses[0].cpm).to.equal(50); + expect(bidResponses[0].creativeId).to.equal('267d72ac3f77a3f447b32cf7ebf20673'); + expect(bidResponses[0].requestId).to.equal('5t5t5t5'); + expect(bidResponses[0].currency).to.equal('USD'); + expect(bidResponses[0].height).to.equal('480'); + expect(bidResponses[0].netRevenue).to.equal(true); + expect(bidResponses[0].referrer).to.equal('http://tokyo-my.mobfox.com/exchange.click.php?h=494ef76d5b0287a8b5ac8724855cb5e0'); + expect(bidResponses[0].ttl).to.equal(360); + expect(bidResponses[0].width).to.equal('320'); }); - it('should add a bid response', function () { - let bidderRequest = { - bidderCode: 'mobfox', - bids: [ - { - bidId: 'bidId1', - bidder: 'mobfox', - params: {}, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] + it('test intercept response with empty server response', () => { + let request = adapter.spec.buildRequests(bidRequest); + let serverResponse = { + request: { + error: 'cannot get response' + } }; - - let requestParams = utils.cloneJson(mockRequestsParams.banner); - bidderRequest.bids[0].params = requestParams; - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter().callBids(bidderRequest); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidResponse1.bidderCode).to.equal('mobfox'); - - expect(bidResponse1.cpm).to.equal(1); - expect(bidResponse1.width).to.equal(bidderRequest.bids[0].sizes[0][0]); - expect(bidResponse1.height).to.equal(bidderRequest.bids[0].sizes[0][1]); - }); - }); + let bidResponses = adapter.spec.interpretResponse(serverResponse, request); + expect(bidResponses.length).to.equal(0); + }) + }) });