diff --git a/modules/silverpushBidAdapter.js b/modules/silverpushBidAdapter.js new file mode 100644 index 00000000000..df81e144380 --- /dev/null +++ b/modules/silverpushBidAdapter.js @@ -0,0 +1,326 @@ +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { mergeDeep } from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { Renderer } from '../src/Renderer.js'; +import { ajax } from '../src/ajax.js'; + +const BIDDER_CODE = 'silverpush'; +const bidderConfig = 'sp_pb_ortb'; +const bidderVersion = '1.0.0'; +const DEFAULT_CURRENCY = 'USD'; + +export const REQUEST_URL = 'https://apac.chocolateplatform.com/bidder/?identifier=prebidchoc'; +export const SP_OUTSTREAM_PLAYER_URL = 'https://xaido.sgp1.cdn.digitaloceanspaces.com/prebid/spoutstream.min.js'; + +const VIDEO_ORTB_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + +export const VIDEO_ORTB_REQUIRED = ['api', 'mimes', 'placement', 'protocols', 'minduration', 'maxduration', 'startdelay']; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + getRequest: function(endpoint) { + ajax(endpoint, null, undefined, {method: 'GET'}); + }, + getOS: function(ua) { + if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.match(/Android/)) { return 'Android'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; } + } +}; + +registerBidder(spec); + +export const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + + if (bidRequest.mediaTypes[VIDEO]) { + imp = buildVideoImp(bidRequest, imp); + } else if (bidRequest.mediaTypes[BANNER]) { + imp = buildBannerImp(bidRequest, imp); + } + + const bidFloor = getBidFloor(bidRequest); + + utils.deepSetValue(imp, 'bidfloor', bidFloor); + + if (bidRequest.params.deals && bidRequest.params.deals.length > 0) { + utils.deepSetValue(imp, 'pmp', { deals: bidRequest.params.deals }); + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + at: 1, + ext: { + bc: `${bidderConfig}_${bidderVersion}` + } + }) + + let userAgent = navigator.userAgent; + utils.deepSetValue(req, 'device.os', spec.getOS(userAgent)); + utils.deepSetValue(req, 'device.devicetype', _isMobile() ? 1 : _isConnectedTV() ? 3 : 2); + + const bid = context.bidRequests[0]; + if (bid.params.publisherId) { + utils.deepSetValue(req, 'site.publisher.id', bid.params.publisherId); + } + + return req; + }, + + bidResponse(buildBidResponse, bid, context) { + let bidResponse = buildBidResponse(bid, context); + + if (bid.ext) { + bidResponse.meta.networkId = bid.ext.dsp_id; + bidResponse.meta.advertiserId = bid.ext.buyer_id; + bidResponse.meta.brandId = bid.ext.brand_id; + } + + if (context.ortbResponse.ext && context.ortbResponse.ext.paf) { + bidResponse.meta.paf = Object.assign({}, context.ortbResponse.ext.paf); + bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); + } + + bidResponse = buildVideoVastResponse(bidResponse) + bidResponse = buildVideoOutstreamResponse(bidResponse, context) + + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + + let fledgeAuctionConfigs = utils.deepAccess(ortbResponse, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return Object.assign({ + bidId, + auctionSignals: {} + }, cfg); + }); + return { + bids: response.bids, + fledgeAuctionConfigs, + } + } else { + return response.bids + } + } +}); + +function isBidRequestValid(bidRequest) { + return (isPublisherIdValid(bidRequest) && (isValidBannerRequest(bidRequest) || isValidVideoRequest(bidRequest))); +} + +function isPublisherIdValid(bidRequest) { + let pubId = utils.deepAccess(bidRequest, 'params.publisherId'); + return (pubId != null && utils.isStr(pubId) && pubId != ''); +} + +function isValidBannerRequest(bidRequest) { + const bannerSizes = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}.sizes`); + + return utils.isArray(bannerSizes) && bannerSizes.length > 0 && bannerSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function isValidVideoRequest(bidRequest) { + const videoSizes = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}.playerSize`); + const PARAM_EXISTS = VIDEO_ORTB_REQUIRED.every(param => utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}.${param}`) != null); + + return PARAM_EXISTS && utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function buildRequests(validBids, bidderRequest) { + let videoBids = validBids.filter(bid => isVideoBid(bid)); + let bannerBids = validBids.filter(bid => isBannerBid(bid)); + let requests = []; + + bannerBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, BANNER)); + }); + + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + + return requests; +} + +function buildVideoImp(bidRequest, imp) { + if (bidRequest.mediaTypes[VIDEO]?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; + } + + const videoMediaType = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`); + const videoSizes = (videoMediaType && videoMediaType.playerSize) || []; + + if (videoSizes && videoSizes.length > 0) { + utils.deepSetValue(imp, 'video.w', videoSizes[0][0]); + utils.deepSetValue(imp, 'video.h', videoSizes[0][1]); + } + + const videoAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`, {}); + const videoBidderParams = utils.deepAccess(bidRequest, `params.${VIDEO}`, {}); + + const videoParams = { ...videoAdUnitParams, ...videoBidderParams }; + + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + utils.deepSetValue(imp, `video.${param}`, videoParams[param]); + } + }); + + return { ...imp }; +} + +function buildBannerImp(bidRequest, imp) { + const bannerSizes = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}.sizes`, []); + + if (bannerSizes && bannerSizes.length > 0) { + utils.deepSetValue(imp, 'banner.w', bannerSizes[0][0]); + utils.deepSetValue(imp, 'banner.h', bannerSizes[0][1]); + } + + return {...imp}; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: REQUEST_URL, + data: CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + } +} + +function buildVideoVastResponse(bidResponse) { + if (bidResponse.mediaType == VIDEO && bidResponse.vastXml) { + bidResponse.vastUrl = bidResponse.vastXml; + } + + return { ...bidResponse } +} + +function buildVideoOutstreamResponse(bidResponse, context) { + if (context.bidRequest.mediaTypes[VIDEO]?.context === 'outstream') { + bidResponse.rendererUrl = SP_OUTSTREAM_PLAYER_URL; + bidResponse.adUnitCode = context.bidRequest.adUnitCode; + + bidResponse.renderer = Renderer.install({ + id: bidResponse.requestId, + adUnitCode: context.bidRequest.adUnitCode, + url: bidResponse.rendererUrl + }); + + bidResponse.renderer.setRender(_renderer(bidResponse)); + + bidResponse.renderer.render(bidResponse); + } + + return {...bidResponse}; +} + +function getBidFloor(bid) { + const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + + if (typeof bid.getFloor !== 'function') { + return utils.deepAccess(bid, 'params.bidFloor', 0.05); + } + + const bidFloor = bid.getFloor({ + currency: currency, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; +} + +function _renderer(bid) { + bid.renderer.push(() => { + if (typeof window.SPOutStreamPlayer === 'function') { + const spoplayer = new window.SPOutStreamPlayer(bid); + + spoplayer.on('ready', () => { + spoplayer.startAd(); + }); + + try { + let vastUrlbt = 'data:text/xml;charset=utf-8;base64,' + btoa(bid.vastUrl.replace(/\\"/g, '"')); + spoplayer.load(vastUrlbt).then(function() { + window.spoplayer = spoplayer; + }).catch(function(reason) { + setTimeout(function() { throw reason; }, 0); + }); + } catch (err) { + utils.logMessage(err); + } + } else { + utils.logMessage(`Silverpush outstream player is not defined`); + } + }); +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +function interpretResponse(resp, req) { + if (!resp.body) { + resp.body = { nbr: 0 }; + } + + return CONVERTER.fromORTB({ request: req.data, response: resp.body }); +} + +function onBidWon(bid) { + if (bid == null) { return; } + if (bid['burl'] == null) { return; } + + let burlMac = bid['burl']; + burlMac = burlMac.replace('$' + '{AUCTION_PRICE}', bid['cpm']); + burlMac = burlMac.replace('$' + '{AUCTION_ID}', bid['auctionId']); + burlMac = burlMac.replace('$' + '{AUCTION_IMP_ID}', bid['requestId']); + burlMac = burlMac.replace('$' + '{AUCTION_AD_ID}', bid['adId']); + burlMac = burlMac.replace('$' + '{AUCTION_SEAT_ID}', bid['seatBidId']); + + spec.getRequest(burlMac); +} + +function _isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function _isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} diff --git a/modules/silverpushBidAdapter.md b/modules/silverpushBidAdapter.md new file mode 100644 index 00000000000..d0af8ba8da8 --- /dev/null +++ b/modules/silverpushBidAdapter.md @@ -0,0 +1,107 @@ +# Overview + +``` +Module Name: Silverpush OpenRTB Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@silverpush.co +``` + +# Description + +Prebid.JS adapter that connects to the Chocolate Ad Exchange. + +*NOTE*: The Silverpush Bidder Adapter requires setup and approval before use. Please reach out to prebid@silverpush.co representative for more details. + +# Bid Parameters + +## Banner/Video + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| -------------- | ----------- | ------------------------------------------ | ------------- | ------------ | +| `publisherId` | required | Publisher id provided by silverpush | "123456" | String | +| `bidFloor` | optional | Minimum price in USD. bidFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50.
| 1.50 | Number | + + + + +# mediaTypes Parameters + +## mediaTypes.banner + +The following banner parameters are supported here so publishers may fully declare their banner inventory: + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| sizes | required | Avalaible sizes supported for banner ad unit | [ [300, 250], [300, 600] ] | [[Integer, Integer], [Integer, Integer]] | + +## mediaTypes.video + + +The following video parameters are supported here so publishers may fully declare their video inventory: + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| context | required | instream or outstream |"outstream" | string | +| playerSize | required | Avalaible sizes supported for video ad unit. | [300, 250] | [Integer, Integer] | +| mimes | required | List of content MIME types supported by the player. | ["video/mp4"]| [String]| +| protocols | required | Supported video bid response protocol values. | [2,3,5,6] | [integers]| +| api | required | Supported API framework values. | [2] | [integers] | +| maxduration | required | Maximum video ad duration in seconds. | 30 | Integer | +| minduration | required | Minimum video ad duration in seconds. | 6 | Integer | +| startdelay | required | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | 0 | Integer | +| placement | required | Placement type for the impression. | 1 | Integer | +| minbitrate | optional | Minimum bit rate in Kbps. | 300 | Integer | +| maxbitrate | optional | Maximum bit rate in Kbps. | 9600 | Integer | +| playbackmethod | optional | Playback methods that may be in use. Only one method is typically used in practice. | [2]| [Integers] | +| linearity | optional | OpenRTB2 linearity. in-strea,overlay... | 1 | Integer | +| skip | optional | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes . | 1 | Integer | +| skipafter | optional | Number of seconds a video must play before skipping is enabled; only applicable if the ad is skippable. | 5 | Integer | +| delivery | optional | OpenRTB2 delivery. Supported delivery methods (e.g., streaming, progressive). If none specified, assume all are supported. | 1 | [Integer] | + + +# Example +```javascript + var adUnits = [{ + code: 'div-1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [300,600] ] + } + }, + bids: [{ + bidder: 'silverpush', + params: { + publisherId: "123456", + bidFloor: 1.2 + } + }] + },{ + code: 'video-1', + mediaTypes: { + video: { + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + context: 'instream', // or 'outstream' + playerSize: [ 640, 480 ], + protocols: [4,5,6,7], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + } + }, + bids: [ + { + bidder: 'silverpush', + params: { + publisherId: "123456", + bidfloor: 2.5 + } + } + ] + } +]; +``` diff --git a/test/spec/modules/silverpushBidAdapter_spec.js b/test/spec/modules/silverpushBidAdapter_spec.js new file mode 100644 index 00000000000..de31135eabe --- /dev/null +++ b/test/spec/modules/silverpushBidAdapter_spec.js @@ -0,0 +1,394 @@ +import { expect } from 'chai'; +import * as utils from 'src/utils'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { REQUEST_URL, SP_OUTSTREAM_PLAYER_URL, CONVERTER, spec } from '../../../modules/silverpushBidAdapter.js'; + +const bannerBid = { + bidder: 'silverpush', + params: { + publisherId: '012345', + bidFloor: 1.5 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 50], + ], + }, + }, + adUnitCode: 'div-gpt-ad-928572628472-0', + bidId: 'dl38fjf9d', + bidderRequestId: 'brid00000000', + auctionId: 'aucid0000000', +}; + +const videoBid = { + bidder: 'silverpush', + params: { + publisherId: '012345', + bidFloor: 0.1 + }, + mediaTypes: { + video: { + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + }, + }, + adUnitCode: 'div-gpt-ad-928572628472-1', + bidId: '281141d3541362', + bidderRequestId: 'brid00000000', + auctionId: 'aucid0000000', +}; + +const bidderRequest = { + auctionId: 'aucid0000000', + bidderRequestId: 'brid00000000', + timeout: 200, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', + } +}; + +const bannerReponse = { + 'id': 'brid00000000', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'ARUYoUZx', + 'impid': 'dl38fjf9d', + 'price': 1.64, + 'adid': 'aaaaadddddddd', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '
', + 'adomain': [ + 'https://www.exampleabc.com' + ], + 'iurl': 'https://example.example.com/2.png', + 'cid': 'aaaaadddddddd', + 'crid': 'aaaaadddddddd', + 'h': 250, + 'w': 300 + } + ] + } + ], + 'bidid': 'ARUYoUZx', + 'cur': 'USD' +} + +const videoResponse = { + 'id': 'brid00000000', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'soCWeklh', + 'impid': '281141d3541362', + 'price': 1.09, + 'adid': 'outstream_video', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '\n', + 'adomain': [ + 'https://www.exampleabc.com' + ], + 'cid': '229369649', + 'crid': 'aaaaadddddddd', + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'bidid': 'soCWeklh', + 'cur': 'USD' +} + +describe('Silverpush Adapter', function () { + describe('isBidRequestValid()', () => { + it('should return false when publisherId is not defined', () => { + const bid = utils.deepClone(bannerBid); + delete bid.params.publisherId; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when publisherId is empty string', () => { + const bid = utils.deepClone(bannerBid); + bid.params.publisherId = ''; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when publisherId is a number', () => { + const bid = utils.deepClone(bannerBid); + bid.params.publisherId = 12345; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when there is no banner in mediaTypes', () => { + const bid = utils.deepClone(bannerBid); + delete bid.mediaTypes.banner; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes for banner are not specified', () => { + const bid = utils.deepClone(bannerBid); + delete bid.mediaTypes.banner.sizes; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(videoBid); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(videoBid); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + expect(spec.isBidRequestValid(videoBid)).to.equal(true); + }); + }); + + describe('buildRequests()', () => { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(bannerBid); + + const [request] = spec.buildRequests([bid], bidderRequest); + const requestData = request.data; + + expect(requestData.imp[0].banner.w).to.equal(300); + expect(requestData.imp[0].banner.h).to.equal(250); + }); + + it('should return default bidfloor when bidFloor is not defined', () => { + const bid = utils.deepClone(bannerBid); + delete bid.params.bidFloor; + + const [request] = spec.buildRequests([bid], bidderRequest); + const requestData = request.data; + + expect(requestData.imp[0].bidfloor).to.equal(0.05); + }); + + it('should contain deals in request if deal is specified in params', () => { + const bid = utils.deepClone(bannerBid); + bid.params.deals = [{ id: 'test' }]; + + const [request] = spec.buildRequests([bid], bidderRequest); + const requestData = request.data; + + expect(requestData.imp[0].pmp.deals).to.equal(bid.params.deals); + }); + + it('should return bidfloor when bidFloor is defined', () => { + const bid = utils.deepClone(bannerBid); + + const [request] = spec.buildRequests([bid], bidderRequest); + const requestData = request.data; + + expect(requestData.imp[0].bidfloor).to.equal(bannerBid.params.bidFloor); + }); + + it('should build correct request for video bid with playerSize', () => { + const bid = utils.deepClone(videoBid); + + const [request] = spec.buildRequests([bid], bidderRequest); + const requestData = request.data; + + expect(requestData.imp[0].video.w).to.equal(1024); + expect(requestData.imp[0].video.h).to.equal(768); + }); + + it('should use bidder video params if they are set', () => { + const videoBidWithParams = utils.deepClone(videoBid); + const bidderVideoParams = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + minduration: 0, + maxduration: 60, + w: 1024, + h: 768, + startdelay: 0 + }; + + videoBidWithParams.params.video = bidderVideoParams; + + const requests = spec.buildRequests([videoBidWithParams], bidderRequest); + const request = requests[0].data; + + expect(request.imp[0]).to.deep.include({ + video: { + ...bidderVideoParams, + w: videoBidWithParams.mediaTypes.video.playerSize[0][0], + h: videoBidWithParams.mediaTypes.video.playerSize[0][1], + }, + }); + }); + }); + + describe('getOS()', () => { + it('shold return correct os name for Windows', () => { + let userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'; + let osName = spec.getOS(userAgent); + + expect(osName).to.equal('Windows'); + }); + + it('shold return correct os name for Mac OS', () => { + let userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'; + let osName = spec.getOS(userAgent); + + expect(osName).to.equal('macOS'); + }); + + it('shold return correct os name for Android', () => { + let userAgent = 'Mozilla/5.0 (Linux; Android 10; SM-G996U Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Mobile Safari/537.36'; + let osName = spec.getOS(userAgent); + + expect(osName).to.equal('Android'); + }); + + it('shold return correct os name for ios', () => { + let userAgent = 'Mozilla/5.0 (iPhone14,3; U; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/19A346 Safari/602.1'; + let osName = spec.getOS(userAgent); + + expect(osName).to.equal('iOS'); + }); + + it('shold return correct os name for Linux', () => { + let userAgent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'; + let osName = spec.getOS(userAgent); + + expect(osName).to.equal('Linux'); + }); + }); + + describe('interpretResponse()', () => { + it('should return nbr to 0 when response not received', () => { + const requests = spec.buildRequests([bannerBid], bidderRequest); + const bids = spec.interpretResponse({ body: null }, requests[0]); + + expect(bids[0]).to.equal(undefined); + }); + + it('should correctly interpret valid banner response', () => { + const response = utils.deepClone(bannerReponse); + const requests = spec.buildRequests([bannerBid], bidderRequest); + const bids = spec.interpretResponse({ body: response }, requests[0]); + + expect(bids[0].ad).to.equal('
'); + expect(bids[0].burl).to.equal('http://0.0.0.0:8181/burl'); + expect(bids[0].cpm).to.equal(1.64); + expect(bids[0].creativeId).to.equal('aaaaadddddddd'); + expect(bids[0].creative_id).to.equal('aaaaadddddddd'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].height).to.equal(250); + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://www.exampleabc.com'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].requestId).to.equal('dl38fjf9d'); + expect(bids[0].seatBidId).to.equal('ARUYoUZx'); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].width).to.equal(300); + }); + + if (FEATURES.VIDEO) { + it('should correctly interpret valid instream video response', () => { + const response = utils.deepClone(videoResponse); + videoBid.mediaTypes.video.context = 'outstream'; + const requests = spec.buildRequests([videoBid], bidderRequest); + const bids = spec.interpretResponse({ body: response }, requests[0]); + + expect(bids[0].vastXml).to.equal('\n'); + expect(bids[0].burl).to.equal('http://0.0.0.0:8181/burl'); + expect(bids[0].cpm).to.equal(1.09); + expect(bids[0].creativeId).to.equal('aaaaadddddddd'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://www.exampleabc.com'); + expect(bids[0].requestId).to.equal('281141d3541362'); + expect(bids[0].seatBidId).to.equal('soCWeklh'); + expect(bids[0].width).to.equal(1024); + expect(bids[0].height).to.equal(768); + }); + + it('should correctly interpret valid outstream video response', () => { + const response = utils.deepClone(videoResponse); + videoBid.mediaTypes.video.context = 'outstream'; + + const requests = spec.buildRequests([videoBid], bidderRequest); + const bids = spec.interpretResponse({ body: response }, requests[0]); + + expect(bids[0].vastXml).to.equal('\n'); + expect(bids[0].rendererUrl).to.equal(SP_OUTSTREAM_PLAYER_URL); + expect(bids[0].renderer.url).to.equal(SP_OUTSTREAM_PLAYER_URL); + expect(bids[0].burl).to.equal('http://0.0.0.0:8181/burl'); + expect(bids[0].cpm).to.equal(1.09); + expect(bids[0].creativeId).to.equal('aaaaadddddddd'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://www.exampleabc.com'); + expect(bids[0].requestId).to.equal('281141d3541362'); + expect(bids[0].seatBidId).to.equal('soCWeklh'); + expect(bids[0].width).to.equal(1024); + expect(bids[0].height).to.equal(768); + }); + } + }); + + describe('onBidWon', function() { + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'getRequest') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('Should not trigger pixel if bid does not contain burl', function() { + const result = spec.onBidWon({}); + + expect(ajaxStub.calledOnce).to.equal(false); + }) + + it('Should trigger pixel with correct macros if bid burl is present', function() { + const result = spec.onBidWon({ + cpm: 1.5, + auctionId: 'auc123', + requestId: 'req123', + adId: 'ad1234', + seatBidId: 'sea123', + burl: 'http://won.foo.bar/trk?ap=${AUCTION_PRICE}&aid=${AUCTION_ID}&imp=${AUCTION_IMP_ID}&adid=${AUCTION_AD_ID}&sid=${AUCTION_SEAT_ID}' + }); + + expect(ajaxStub.calledOnceWith('http://won.foo.bar/trk?ap=1.5&aid=auc123&imp=req123&adid=ad1234&sid=sea123')).to.equal(true); + }) + }) +});