diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js new file mode 100644 index 00000000000..295be160ee9 --- /dev/null +++ b/modules/kueezBidAdapter.js @@ -0,0 +1,470 @@ +import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_ENDPOINT = 'https://hb.kueezssp.com/hb-kz-multi'; +const BIDDER_TEST_ENDPOINT = 'https://hb.kueezssp.com/hb-multi-kz-test' +const BIDDER_CODE = 'kueez'; +const MAIN_CURRENCY = 'USD'; +const MEDIA_TYPES = [BANNER, VIDEO]; +const TTL = 420; +const VERSION = '1.0.0'; +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: VERSION, + supportedMediaTypes: MEDIA_TYPES, + isBidRequestValid: function (bidRequest) { + return validateParams(bidRequest); + }, + buildRequests: function (validBidRequests, bidderRequest) { + const [ sharedParams ] = validBidRequests; + const testMode = sharedParams.params.testMode; + const bidsToSend = prepareBids(validBidRequests, sharedParams, bidderRequest); + + return { + method: 'POST', + url: getBidderEndpoint(testMode), + data: bidsToSend + } + }, + interpretResponse: function ({body}) { + const bidResponses = body?.bids; + + if (!bidResponses || !bidResponses.length) { + return []; + } + + return parseBidResponses(bidResponses); + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.params.userSyncURL + }); + } + } + return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +/** + * Get the encoded value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !isEmpty(val) ? encodeURIComponent(val) : ''; +} + +/** + * get device type + * @returns {string} + */ +function getDeviceType() { + const ua = navigator.userAgent; + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i + .test(ua.toLowerCase())) { + return '5'; + } + if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i + .test(ua.toLowerCase())) { + return '4'; + } + if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i + .test(ua.toLowerCase())) { + return '3'; + } + return '1'; +} + +/** + * Get floor price + * @param bid {bid} + * @param mediaType {string} + * @returns {Number} + */ +function getFloorPrice(bid, mediaType) { + let floor = 0; + + if (isFn(bid.getFloor)) { + let floorResult = bid.getFloor({ + currency: MAIN_CURRENCY, + mediaType: mediaType, + size: '*' + }); + floor = floorResult.currency === MAIN_CURRENCY && floorResult.floor ? floorResult.floor : 0; + } + + return floor; +} + +/** + * Get the ad sizes array from the bid + * @param bid {bid} + * @param mediaType {string} + * @returns {Array} + */ +function getSizesArray(bid, mediaType) { + let sizes = [] + + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizes = bid.mediaTypes[mediaType].sizes; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizes = bid.sizes; + } + + return sizes; +} + +/** + * Get the preferred user-sync method + * @param filterSettings {filterSettings} + * @param bidderCode {string} + * @returns {string} + */ +function getSyncMethod(filterSettings, bidderCode) { + const iframeConfigs = ['all', 'iframe']; + const pixelConfig = 'image'; + if (filterSettings && iframeConfigs.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfig] || isSyncMethodAllowed(filterSettings[pixelConfig], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check sync rule support + * @param filterSetting {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(filterSetting, bidderCode) { + if (!filterSetting) { + return false; + } + const bidders = isArray(filterSetting.bidders) ? filterSetting.bidders : [bidderCode]; + return filterSetting.filter === 'include' && contains(bidders, bidderCode); +} + +/** + * Get the bidder endpoint + * @param testMode {boolean} + * @returns {string} + */ +function getBidderEndpoint(testMode) { + return testMode ? BIDDER_TEST_ENDPOINT : BIDDER_ENDPOINT; +} + +/** + * Generates the bidder parameters + * @param validBidRequests {Array} + * @param bidderRequest {bidderRequest} + * @returns {Array} + */ +function generateBidParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + + return bidsArray; +} + +/** + * Generate bid specific parameters + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} bid specific params object + */ +function generateBidParameters(bid, bidderRequest) { + const {params} = bid; + const mediaType = isBanner(bid) ? BANNER : VIDEO; + const sizesArray = getSizesArray(bid, mediaType); + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); + const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); + const paramsFloorPrice = isNaN(params.floorPrice) ? 0 : params.floorPrice; + + const bidObject = { + adUnitCode: getBidIdParameter('adUnitCode', bid), + bidId: getBidIdParameter('bidId', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + floorPrice: Math.max(getFloorPrice(bid, mediaType), paramsFloorPrice), + mediaType, + sizes: sizesArray, + transactionId: getBidIdParameter('transactionId', bid) + }; + + if (pos) { + bidObject.pos = pos; + } + + if (gpid) { + bidObject.gpid = gpid; + } + + if (placementId) { + bidObject.placementId = placementId; + } + + if (mediaType === VIDEO) { + populateVideoParams(bidObject, bid); + } + + return bidObject; +} + +/** + * Checks if the media type is a banner + * @param bid {bid} + * @returns {boolean} + */ +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; +} + +/** + * Generate params that are common between all bids + * @param sharedParams {sharedParams} + * @param bidderRequest {bidderRequest} + * @returns {object} the common params object + */ +function generateSharedParams(sharedParams, bidderRequest) { + const {bidderCode} = bidderRequest; + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const domain = window.location.hostname; + const generalBidParams = getBidIdParameter('params', sharedParams); + const userIds = getBidIdParameter('userId', sharedParams); + const ortb2Metadata = config.getConfig('ortb2') || {}; + const timeout = config.getConfig('bidderTimeout'); + + const params = { + adapter_version: VERSION, + auction_start: timestamp(), + device_type: getDeviceType(), + dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, + publisher_id: generalBidParams.org, + publisher_name: domain, + session_id: getBidIdParameter('auctionId', sharedParams), + site_domain: domain, + tmax: timeout, + ua: navigator.userAgent, + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$' + } + + if (syncEnabled) { + const allowedSyncMethod = getSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + params.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + params.gdpr = bidderRequest.gdprConsent.gdprApplies; + params.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (bidderRequest.uspConsent) { + params.us_privacy = bidderRequest.uspConsent; + } + + if (generalBidParams.ifa) { + params.ifa = generalBidParams.ifa; + } + + if (ortb2Metadata.site) { + params.site_metadata = JSON.stringify(ortb2Metadata.site); + } + + if (ortb2Metadata.user) { + params.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (bidderRequest && bidderRequest.refererInfo) { + params.page_url = config.getConfig('pageUrl') || window.location.href; + params.referrer = bidderRequest.refererInfo.referer; + } + + if (sharedParams.schain) { + params.schain = getSupplyChain(sharedParams.schain); + } + + if (userIds) { + params.userIds = JSON.stringify(userIds); + } + + return params +} + +/** + * Validates the bidder params + * @param bidRequest {bidRequest} + * @returns {boolean} + */ +function validateParams(bidRequest) { + let isValid = true; + + if (!bidRequest.params) { + logWarn('Kueez adapter - missing params'); + isValid = false; + } + + if (!bidRequest.params.org) { + logWarn('Kueez adapter - org is a required param'); + isValid = false; + } + + return isValid; +} + +/** + * Validates the bidder params + * @param validBidRequests {Array} + * @param sharedParams {sharedParams} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function prepareBids(validBidRequests, sharedParams, bidderRequest) { + return { + params: generateSharedParams(sharedParams, bidderRequest), + bids: generateBidParams(validBidRequests, bidderRequest) + } +} + +function getPlaybackMethod(bid) { + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + + if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { + return playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + return playbackMethod; + } +} + +function populateVideoParams(params, bid) { + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + const placement = deepAccess(bid, `mediaTypes.video.placement`); + const playbackMethod = getPlaybackMethod(bid); + const skip = deepAccess(bid, `mediaTypes.video.skip`); + + if (linearity) { + params.linearity = linearity; + } + + if (maxDuration) { + params.maxDuration = maxDuration; + } + + if (minDuration) { + params.minDuration = minDuration; + } + + if (placement) { + params.placement = placement; + } + + if (playbackMethod) { + params.playbackMethod = playbackMethod; + } + + if (skip) { + params.skip = skip; + } +} + +/** + * Processes the bid responses + * @param bids {Array} + * @returns {Array} + */ +function parseBidResponses(bids) { + return bids.map(bid => { + const bidResponse = { + cpm: bid.cpm, + creativeId: bid.requestId, + currency: bid.currency || MAIN_CURRENCY, + height: bid.height, + mediaType: bid.mediaType, + meta: { + mediaType: bid.mediaType + }, + netRevenue: bid.netRevenue || true, + nurl: bid.nurl, + requestId: bid.requestId, + ttl: bid.ttl || TTL, + width: bid.width + }; + + if (bid.adomain && bid.adomain.length) { + bidResponse.meta.advertiserDomains = bid.adomain; + } + + if (bid.mediaType === VIDEO) { + bidResponse.vastXml = bid.vastXml; + } else if (bid.mediaType === BANNER) { + bidResponse.ad = bid.ad; + } + + return bidResponse; + }); +} diff --git a/modules/kueezBidAdapter.md b/modules/kueezBidAdapter.md new file mode 100644 index 00000000000..c2afbf0798c --- /dev/null +++ b/modules/kueezBidAdapter.md @@ -0,0 +1,73 @@ +#Overview + +Module Name: Kueez Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid@kueez.com + +# Description + +The Kueez adapter requires setup and approval from the Kueez team. Please reach out to prebid@kueez.com for more information. + +The adapter supports Banner and Video(instream) media types. + +# Bid Parameters + +## Video + +| Name | Scope | Type | Description | Example +|---------------| ----- | ---- |-------------------------------------------------------------------| ------- +| `org` | required | String | the organization Id provided by your Kueez representative | "test-publisher-id" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 1.50 +| `placementId` | optional | String | A unique placement identifier | "12345678" +| `testMode` | optional | Boolean | This activates the test mode | false + +# Test Parameters + +```javascript +var adUnits = [{ + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bids: [{ + bidder: 'kueez', + params: { + org: 'test-org-id', // Required + floorPrice: 0.10, // Optional + placementId: '12345678', // Optional + testMode: true // Optional + } + }] +}, + { + code: 'dfp-video-div', + sizes: [ + [640, 480] + ], + mediaTypes: { + video: { + playerSize: [ + [640, 480] + ], + context: 'instream' + } + }, + bids: [{ + bidder: 'kueez', + params: { + org: 'test-org-id', // Required + floorPrice: 2.00, // Optional + placementId: '12345678', // Optional + testMode: true // Optional + } + }] + } +]; +``` diff --git a/test/spec/modules/kueezBidAdapter_spec.js b/test/spec/modules/kueezBidAdapter_spec.js new file mode 100644 index 00000000000..16440965765 --- /dev/null +++ b/test/spec/modules/kueezBidAdapter_spec.js @@ -0,0 +1,479 @@ +import { expect } from 'chai'; +import { spec } from 'modules/kueezBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://hb.kueezssp.com/hb-kz-multi'; +const TEST_ENDPOINT = 'https://hb.kueezssp.com/hb-multi-kz-test'; +const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ + +describe('kueezBidAdapter', 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 () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'test-publisher-id' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'test-publisher-id' + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream' + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'test-publisher-id' + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'test-publisher-id', + 'testMode': true + }, + 'bidId': '5wfg9887sd5478', + 'bidderRequestId': 'op87952ewq8567', + 'auctionId': '87se98rt-5789-8735-2546-t98yh5678231', + } + ]; + + const bidderRequest = { + bidderCode: 'kueez', + } + const placementId = '12345678'; + + it('sends the placementId to ENDPOINT via POST', function () { + bidRequests[0].params.placementId = placementId; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].placementId).to.equal(placementId); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send the correct bid Id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('5wfg9887sd5478'); + }); + + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) + expect(request.data.bids[1].sizes).to.be.an('array'); + expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + }); + + it('should send the correct media type', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].mediaType).to.equal(VIDEO) + expect(request.data.bids[1].mediaType).to.equal(BANNER) + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); + config.setConfig({ + userSync: { + syncEnabled: true, + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + }); + + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); + }); + + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); + }); + }); + + describe('interpretResponse', function () { + const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + cpm: 12.5, + creativeId: '21e12606d47ba7', + currency: 'USD', + height: 480, + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + netRevenue: true, + nurl: 'http://example.com/win/1234', + requestId: '21e12606d47ba7', + ttl: TTL, + width: 640, + vastXml: '' + }; + + const expectedBannerResponse = { + cpm: 12.5, + creativeId: '21e12606d47ba7', + currency: 'USD', + height: 480, + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + netRevenue: true, + nurl: 'http://example.com/win/1234', + requestId: '21e12606d47ba7', + ttl: TTL, + width: 640, + ad: '""' + }; + + it('should get correct bid response', function () { + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); + expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + } + }; + + const iframeSyncResponse = { + body: { + params: { + userSyncURL: 'https://iframe-sync-url.test' + } + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'org': 'test-publisher-id' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) +});