diff --git a/modules/shBidAdapter.js b/modules/shBidAdapter.js new file mode 100644 index 00000000000..94d28770548 --- /dev/null +++ b/modules/shBidAdapter.js @@ -0,0 +1,184 @@ +import * as utils from '../src/utils'; +import { config } from '../src/config'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { VIDEO, BANNER } from '../src/mediaTypes'; + +const PROD_ENDPOINT = 'https://bs1.showheroes.com/api/v1/bid'; +const STAGE_ENDPOINT = 'https://bid-service.stage.showheroes.com/api/v1/bid'; +const PROD_PUBLISHER_TAG = 'https://static.showheroes.com/publishertag.js'; +const STAGE_PUBLISHER_TAG = 'https://pubtag.stage.showheroes.com/publishertag.js'; +const PROD_VL = 'https://video-library.showheroes.com'; +const STAGE_VL = 'https://video-library.stage.showheroes.com'; +const BIDDER_CODE = 'showheroes-bs'; +const TTL = 300; + +export const spec = { + code: BIDDER_CODE, + aliases: ['showheroesBs'], + supportedMediaTypes: [VIDEO, BANNER], + isBidRequestValid: function(bid) { + return !!bid.params.playerId; + }, + buildRequests: function(validBidRequests, bidderRequest) { + const pageURL = validBidRequests[0].params.contentPageUrl || bidderRequest.refererInfo.referer; + const isStage = !!validBidRequests[0].params.stage; + const isBanner = !!validBidRequests[0].mediaTypes.banner; + + let adUnits = validBidRequests.map((bid) => { + const vpaidMode = utils.getBidIdParameter('vpaidMode', bid.params); + + let sizes = bid.sizes.length === 1 ? bid.sizes[0] : bid.sizes; + if (sizes && !sizes.length) { + let mediaSize; + if (!isBanner) { + mediaSize = bid.mediaTypes.video.playerSize; + } else { + mediaSize = bid.mediaTypes.banner.sizes; + } + if (utils.isArray(mediaSize[0])) { + sizes = mediaSize[0]; + } else if (utils.isNumber(mediaSize[0])) { + sizes = mediaSize; + } + } + + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + + let streamType = 2; + + if (vpaidMode && context === 'instream') { + streamType = 1; + } + if (context === 'outstream' || isBanner) { + streamType = 5; + } + + return { + type: streamType, + bidId: bid.bidId, + mediaType: isBanner ? BANNER : VIDEO, + playerId: utils.getBidIdParameter('playerId', bid.params), + auctionId: bidderRequest.auctionId, + bidderCode: BIDDER_CODE, + gdprConsent: bidderRequest.gdprConsent, + start: +new Date(), + timeout: 3000, + video: { + width: sizes[0], + height: sizes[1] + }, + }; + }); + + return { + url: isStage ? STAGE_ENDPOINT : PROD_ENDPOINT, + method: 'POST', + options: {contentType: 'application/json', accept: 'application/json'}, + data: { + 'user': [], + 'meta': { + 'pageURL': encodeURIComponent(pageURL), + 'vastCacheEnabled': (!!config.getConfig('cache') && !isBanner) || false, + 'isDesktop': utils.getWindowTop().document.documentElement.clientWidth > 700, + 'stage': isStage || undefined + }, + 'requests': adUnits, + 'debug': validBidRequests[0].params.debug || false, + } + }; + }, + interpretResponse: function(response, request) { + return createBids(response.body, request.data); + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + + if (!serverResponses.length || !serverResponses[0].body.userSync) { + return syncs; + } + + const userSync = serverResponses[0].body.userSync; + + if (syncOptions.iframeEnabled) { + (userSync.iframes || []).forEach(url => { + syncs.push({ + type: 'iframe', + url + }); + }); + } + + if (syncOptions.pixelEnabled) { + (userSync.pixels || []).forEach(url => { + syncs.push({ + type: 'image', + url + }); + }); + } + return syncs; + }, +}; + +function createBids(bidRes, reqData) { + if (bidRes && (!Array.isArray(bidRes.bids) || bidRes.bids.length < 1)) { + return []; + } + + const bids = []; + const bidMap = {}; + (reqData.requests || []).forEach((bid) => { + bidMap[bid.bidId] = bid; + }); + + bidRes.bids.forEach(function (bid) { + const reqBid = bidMap[bid.bidId]; + let bidUnit = {}; + bidUnit.cpm = bid.cpm; + bidUnit.requestId = bid.bidId; + bidUnit.currency = bid.currency; + bidUnit.mediaType = reqBid.mediaType || VIDEO; + bidUnit.ttl = TTL; + bidUnit.creativeId = 'c_' + bid.bidId; + bidUnit.netRevenue = true; + bidUnit.width = bid.video.width; + bidUnit.height = bid.video.height; + if (bid.vastXml) { + bidUnit.vastXml = bid.vastXml; + bidUnit.adResponse = { + content: bid.vastXml, + }; + } + if (bid.vastTag) { + bidUnit.vastUrl = bid.vastTag; + } + if (reqBid.mediaType === BANNER) { + bidUnit.ad = getBannerHtml(bid, reqBid, reqData); + } + bids.push(bidUnit); + }); + + return bids; +} + +function getBannerHtml (bid, reqBid, reqData) { + const isStage = !!reqData.meta.stage; + const pubTag = isStage ? STAGE_PUBLISHER_TAG : PROD_PUBLISHER_TAG; + const vlHost = isStage ? STAGE_VL : PROD_VL; + return ` +
+ + + + + `; +} + +registerBidder(spec); diff --git a/modules/shBidAdapter.md b/modules/shBidAdapter.md new file mode 100644 index 00000000000..b1ca1782725 --- /dev/null +++ b/modules/shBidAdapter.md @@ -0,0 +1,69 @@ +# Overview + +Module Name: ShowHeroes Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: tech@showheroes.com + +# Description + +Module that connects to ShowHeroes demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + } + }, + bids: [ + { + bidder: "showheroes-bs", + params: { + playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', + vpaidMode: true // by default is 'false' + } + } + ] + }, + { + code: 'video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream', + } + }, + bids: [ + { + bidder: "showheroes-bs", + params: { + playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', + vpaidMode: true // by default is 'false' + } + } + ] + }, + { + code: 'banner', + mediaTypes: { + banner: { + sizes: [[640, 480]], + } + }, + bids: [ + { + bidder: "showheroes-bs", + params: { + playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/shBidAdapter_spec.js b/test/spec/modules/shBidAdapter_spec.js new file mode 100644 index 00000000000..588d4c54150 --- /dev/null +++ b/test/spec/modules/shBidAdapter_spec.js @@ -0,0 +1,187 @@ +import {expect} from 'chai' +import {spec} from 'modules/shBidAdapter' +import {newBidder} from 'src/adapters/bidderFactory' +import {VIDEO, BANNER} from 'src/mediaTypes' + +const bidderRequest = { + refererInfo: { + referer: 'http://example.com' + } +} + +const gdpr = { + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': true + } +} + +const bidRequestVideo = { + 'bidder': 'showheroes-bs', + 'params': { + 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[640, 480]], + 'bidId': '38b373e1e31c18', + 'bidderRequestId': '12e3ade2543ba6', + 'auctionId': '43aa080090a47f', +} + +const bidRequestVideoVpaid = { + 'bidder': 'showheroes-bs', + 'params': { + 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', + 'vpaidMode': true, + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[640, 480]], + 'bidId': '38b373e1e31c18', + 'bidderRequestId': '12e3ade2543ba6', + 'auctionId': '43aa080090a47f', +} + +const bidRequestBanner = { + 'bidder': 'showheroes-bs', + 'params': { + 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 360]] + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[640, 480]], + 'bidId': '38b373e1e31c18', + 'bidderRequestId': '12e3ade2543ba6', + 'auctionId': '43aa080090a47f', +} + +describe('shBidAdapter', function () { + const adapter = newBidder(spec) + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const request = { + 'params': { + 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', + } + } + expect(spec.isBidRequestValid(request)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + const request = { + 'params': {} + } + expect(spec.isBidRequestValid(request)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests([bidRequestVideo], bidderRequest) + expect(request.method).to.equal('POST') + }) + + it('should attach valid params to the payload when type is video', function () { + const request = spec.buildRequests([bidRequestVideo], bidderRequest) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload).to.have.property('mediaType', VIDEO); + expect(payload).to.have.property('type', 2); + }) + + it('should attach valid params to the payload when type is video & vpaid mode on', function () { + const request = spec.buildRequests([bidRequestVideoVpaid], bidderRequest) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload).to.have.property('mediaType', VIDEO); + expect(payload).to.have.property('type', 1); + }) + + it('should attach valid params to the payload when type is banner', function () { + const request = spec.buildRequests([bidRequestBanner], bidderRequest) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload).to.have.property('mediaType', BANNER); + expect(payload).to.have.property('type', 5); + }) + + it('passes gdpr if present', function () { + const request = spec.buildRequests([bidRequestVideo], {...bidderRequest, ...gdpr}) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload.gdprConsent).to.eql(gdpr.gdprConsent) + }) + }) + + describe('interpretResponse', function () { + it('handles nobid responses', function () { + expect(spec.interpretResponse({body: {}}, {data: {meta: {}}}).length).to.equal(0) + expect(spec.interpretResponse({body: []}, {data: {meta: {}}}).length).to.equal(0) + }) + + const response = { + 'bids': [{ + 'cpm': 5, + 'currency': 'EUR', + 'bidId': '38b373e1e31c18', + 'video': {'width': 640, 'height': 480}, + 'vastTag': 'https:\/\/video-library.stage.showheroes.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', + }], + } + + it('should get correct bid response when type is video', function () { + const request = spec.buildRequests([bidRequestVideo], bidderRequest) + const expectedResponse = [ + { + 'cpm': 5, + 'creativeId': 'c_38b373e1e31c18', + 'currency': 'EUR', + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'netRevenue': true, + 'vastUrl': 'https://video-library.stage.showheroes.com/commercial/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', + 'requestId': '38b373e1e31c18', + 'ttl': 300, + } + ] + + const result = spec.interpretResponse({'body': response}, request) + expect(result).to.deep.equal(expectedResponse) + }) + + it('should get correct bid response when type is banner', function () { + const request = spec.buildRequests([bidRequestBanner], bidderRequest) + + const result = spec.interpretResponse({'body': response}, request) + expect(result[0]).to.have.property('mediaType', BANNER); + expect(result[0].ad).to.include('