From 62d82a2fa9295817df7e2de11e3bb836900a24ae Mon Sep 17 00:00:00 2001 From: Dmitriy Labuzov Date: Wed, 1 Jul 2020 19:51:22 +0300 Subject: [PATCH] Instream video support for Yieldmo adapter --- modules/yieldmoBidAdapter.js | 398 +++++++++++++++--- modules/yieldmoBidAdapter.md | 75 +++- test/spec/modules/yieldmoBidAdapter_spec.js | 432 ++++++++++---------- 3 files changed, 611 insertions(+), 294 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 829b573ffd92..0915c8d9c19c 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,102 +1,133 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js-pure/features/array/includes'; const BIDDER_CODE = 'yieldmo'; const CURRENCY = 'USD'; const TIME_TO_LIVE = 300; const NET_REVENUE = true; -const SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const BANNER_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const VIDEO_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; +const OPENRTB_VIDEO_BIDPARAMS = ['placement', 'startdelay', 'skipafter', + 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos']; +const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const localWindow = utils.getWindowTop(); export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner'], + supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ isBidRequestValid: function (bid) { - return !!(bid && bid.adUnitCode && bid.bidId); + return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && + validateVideoParams(bid)); }, + /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { - let serverRequest = { - p: [], - page_url: bidderRequest.refererInfo.referer, - bust: new Date().getTime().toString(), - pr: bidderRequest.refererInfo.referer, - scrd: localWindow.devicePixelRatio || 0, - dnt: getDNT(), - description: getPageDescription(), - title: localWindow.document.title || '', - w: localWindow.innerWidth, - h: localWindow.innerHeight, - userConsent: JSON.stringify({ - // case of undefined, stringify will remove param - gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', - cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' - }), - us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' - }; - - const mtp = window.navigator.maxTouchPoints; - if (mtp) { - serverRequest.mtp = mtp; - } + const bannerBidRequests = bidRequests.filter(request => hasBannerMediaType(request)); + const videoBidRequests = bidRequests.filter(request => hasVideoMediaType(request)); + + let serverRequests = []; + if (bannerBidRequests.length > 0) { + let serverRequest = { + pbav: '$prebid.version$', + p: [], + page_url: bidderRequest.refererInfo.referer, + bust: new Date().getTime().toString(), + pr: bidderRequest.refererInfo.referer, + scrd: localWindow.devicePixelRatio || 0, + dnt: getDNT(), + description: getPageDescription(), + title: localWindow.document.title || '', + w: localWindow.innerWidth, + h: localWindow.innerHeight, + userConsent: JSON.stringify({ + // case of undefined, stringify will remove param + gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' + }), + us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' + }; - bidRequests.forEach(request => { - serverRequest.p.push(addPlacement(request)); - const pubcid = getId(request, 'pubcid'); - if (pubcid) { - serverRequest.pubcid = pubcid; - } else if (request.crumbs) { - if (request.crumbs.pubcid) { + const mtp = window.navigator.maxTouchPoints; + if (mtp) { + serverRequest.mtp = mtp; + } + + bannerBidRequests.forEach(request => { + serverRequest.p.push(addPlacement(request)); + const pubcid = getId(request, 'pubcid'); + if (pubcid) { + serverRequest.pubcid = pubcid; + } else if (request.crumbs && request.crumbs.pubcid) { serverRequest.pubcid = request.crumbs.pubcid; } - } - const tdid = getId(request, 'tdid'); - if (tdid) { - serverRequest.tdid = tdid; - } - const criteoId = getId(request, 'criteoId'); - if (criteoId) { - serverRequest.cri_prebid = criteoId; - } - if (request.schain) { - serverRequest.schain = - JSON.stringify(request.schain); - } - }); - serverRequest.p = '[' + serverRequest.p.toString() + ']'; - return { - method: 'GET', - url: SERVER_ENDPOINT, - data: serverRequest - }; + const tdid = getId(request, 'tdid'); + if (tdid) { + serverRequest.tdid = tdid; + } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } + if (request.schain) { + serverRequest.schain = JSON.stringify(request.schain); + } + }); + serverRequest.p = '[' + serverRequest.p.toString() + ']'; + serverRequests.push({ + method: 'GET', + url: BANNER_SERVER_ENDPOINT, + data: serverRequest + }); + } + + if (videoBidRequests.length > 0) { + const serverRequest = openRtbRequest(videoBidRequests, bidderRequest); + serverRequests.push({ + method: 'POST', + url: VIDEO_SERVER_ENDPOINT, + data: serverRequest + }); + } + return serverRequests; }, + /** * Makes Yieldmo Ad Server response compatible to Prebid specs - * @param serverResponse successful response from Ad Server + * @param {ServerResponse} serverResponse successful response from Ad Server + * @param {ServerRequest} bidRequest * @return {Bid[]} an array of bids */ - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { let bids = []; - let data = serverResponse.body; + const data = serverResponse.body; if (data.length > 0) { data.forEach(response => { - if (response.cpm && response.cpm > 0) { - bids.push(createNewBid(response)); + if (response.cpm > 0) { + bids.push(createNewBannerBid(response)); } }); } + if (data.seatbid) { + const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); + seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); + } return bids; }, + getUserSyncs: function () { return []; } @@ -107,6 +138,20 @@ registerBidder(spec); * Helper Functions ***************************************/ +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.video'); +} + /** * Adds placement information to array * @param request bid request @@ -129,10 +174,10 @@ function addPlacement(request) { } /** - * creates a new bid with response information + * creates a new banner bid with response information * @param response server response */ -function createNewBid(response) { +function createNewBannerBid(response) { return { requestId: response['callback_id'], cpm: response.cpm, @@ -146,6 +191,27 @@ function createNewBid(response) { }; } +/** + * creates a new video bid with response information + * @param response openRTB server response + * @param bidRequest server request + */ +function createNewVideoBid(response, bidRequest) { + const imp = (utils.deepAccess(bidRequest, 'data.imp') || []).find(imp => imp.id === response.impid); + return { + requestId: imp.id, + cpm: response.price, + width: imp.video.w, + height: imp.video.h, + creativeId: response.crid || response.adid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + mediaType: VIDEO, + ttl: TIME_TO_LIVE, + vastXml: response.adm + }; +} + /** * Detects whether dnt is true * @returns true if user enabled dnt @@ -177,4 +243,216 @@ function getPageDescription() { */ function getId(request, idType) { return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; + return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; +} + +/** + * @param {BidRequest[]} bidRequests bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB request object + */ +function openRtbRequest(bidRequests, bidderRequest) { + let openRtbRequest = { + id: bidRequests[0].bidderRequestId, + imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), + site: openRtbSite(bidRequests[0], bidderRequest), + device: openRtbDevice(), + badv: bidRequests[0].params.badv || [], + bcat: bidRequests[0].params.bcat || [], + ext: { + prebid: '$prebid.version$', + } + }; + + populateOpenRtbGdpr(openRtbRequest, bidderRequest); + + return openRtbRequest; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return Object OpenRTB's 'imp' (impression) object + */ +function openRtbImpression(bidRequest) { + const videoReq = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const size = extractPlayerSize(bidRequest); + const imp = { + id: bidRequest.bidId, + tagid: bidRequest.adUnitCode, + bidfloor: bidRequest.params.bidfloor || 0, + ext: { + placement_id: bidRequest.params.placementId + }, + video: { + w: size[0], + h: size[1], + mimes: videoReq.mimes, + linearity: 1 + } + }; + + const videoParams = utils.deepAccess(bidRequest, 'params.video'); + Object.keys(videoParams) + .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) + .forEach(param => imp.video[param] = videoParams[param]); + + if (videoParams.skippable) { + imp.video.skip = 1; + } + + return imp; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return [number, number] || null Player's width and height, or undefined otherwise. + */ +function extractPlayerSize(bidRequest) { + const sizeArr = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + if (utils.isArrayOfNums(sizeArr, 2)) { + return sizeArr; + } else if (utils.isArray(sizeArr) && utils.isArrayOfNums(sizeArr[0], 2)) { + return sizeArr[0]; + } + return null; +} + +/** + * @param {BidRequest} bidRequest bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB's 'site' object + */ +function openRtbSite(bidRequest, bidderRequest) { + let result = {}; + + const loc = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + if (!utils.isEmpty(loc)) { + result.page = `${loc.protocol}://${loc.hostname}${loc.pathname}`; + } + + if (self === top && document.referrer) { + result.ref = document.referrer; + } + + const keywords = document.getElementsByTagName('meta')['keywords']; + if (keywords && keywords.content) { + result.keywords = keywords.content; + } + + const siteParams = utils.deepAccess(bidRequest, 'params.site'); + if (siteParams) { + Object.keys(siteParams) + .filter(param => includes(OPENRTB_VIDEO_SITEPARAMS, param)) + .forEach(param => result[param] = siteParams[param]); + } + return result; +} + +/** + * @return Object OpenRTB's 'device' object + */ +function openRtbDevice() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Updates openRtbRequest with GDPR info from bidderRequest, if present. + * @param {Object} openRtbRequest OpenRTB's request to update. + * @param {BidderRequest} bidderRequest bidder request object. + */ +function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { + const gdpr = bidderRequest.gdprConsent; + if (gdpr && 'gdprApplies' in gdpr) { + utils.deepSetValue(openRtbRequest, 'regs.ext.gdpr', gdpr.gdprApplies ? 1 : 0); + utils.deepSetValue(openRtbRequest, 'user.ext.consent', gdpr.consentString); + } + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + utils.deepSetValue(openRtbRequest, 'regs.ext.us_privacy', uspConsent); + } +} + +/** + * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function validateVideoParams(bid) { + if (!hasVideoMediaType(bid)) { + return true; + } + + const paramRequired = (paramStr, value, conditionStr) => { + let error = `"${paramStr}" is required`; + if (conditionStr) { + error += ' when ' + conditionStr; + } + throw new Error(error); + } + + const paramInvalid = (paramStr, value, expectedStr) => { + expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; + value = JSON.stringify(value); + throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); + } + + const isDefined = val => typeof val !== 'undefined'; + const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { + const value = utils.deepAccess(bid, fieldPath); + if (!validateCb(value)) { + errorCb(fieldPath, value, errorCbParam); + } + return value; + } + + try { + validate('params.placementId', val => !utils.isEmpty(val), paramRequired); + + validate('mediaTypes.video.playerSize', val => utils.isArrayOfNums(val, 2) || + (utils.isArray(val) && val.every(v => utils.isArrayOfNums(v, 2))), + paramInvalid, 'array of 2 integers, ex: [640,480] or [[640,480]]'); + + validate('mediaTypes.video.mimes', val => isDefined(val), paramRequired); + validate('mediaTypes.video.mimes', val => utils.isArray(val) && val.every(v => utils.isStr(v)), paramInvalid, + 'array of strings, ex: ["video/mp4"]'); + + validate('params.video', val => !utils.isEmpty(val), paramRequired); + + const placement = validate('params.video.placement', val => isDefined(val), paramRequired); + validate('params.video.placement', val => val >= 1 && val <= 5, paramInvalid); + if (placement === 1) { + validate('params.video.startdelay', val => isDefined(val), + (field, v) => paramRequired(field, v, 'placement == 1')); + validate('params.video.startdelay', val => utils.isNumber(val), paramInvalid, 'number, ex: 5'); + } + + validate('params.video.protocols', val => isDefined(val), paramRequired); + validate('params.video.protocols', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.api', val => isDefined(val), paramRequired); + validate('params.video.api', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.playbackmethod', val => !isDefined(val) || utils.isArrayOfNums(val), paramInvalid, + 'array of integers, ex: [2,6]'); + + validate('params.video.maxduration', val => isDefined(val), paramRequired); + validate('params.video.maxduration', val => utils.isInteger(val), paramInvalid); + validate('params.video.minduration', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.skippable', val => !isDefined(val) || utils.isBoolean(val), paramInvalid); + validate('params.video.skipafter', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.pos', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.badv', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["ford.com","pepsi.com"]'); + validate('params.bcat', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["IAB1-5","IAB1-6"]'); + return true; + } catch (e) { + utils.logError(e.message); + return false; + } } diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index 0f86d2507d14..1b8b7b1b7413 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -11,26 +11,61 @@ Note: Our ads will only render in mobile Connects to Yieldmo Ad Server for bids. -Yieldmo bid adapter supports Banner. +Yieldmo bid adapter supports Banner and Video. # Test Parameters + +## Banner + +Sample banner ad unit config: +```javascript +var adUnits = [{ // Banner adUnit + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) + bidFloor: .28 // optional param + } + }] +}]; +``` + +## Video + +Sample instream video ad unit config: +```javascript +var adUnits = [{ // Video adUnit + code: 'div-video-ad-1234567890', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', + mimes: ['video/mp4'] // required, array of strings + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1524592390382976659', // required + video: { + placement: 1, // required, integer + maxduration: 30, // required, integer + minduration: 15, // optional, integer + pos: 1, // optional, integer + startdelay: 10, // required if placement == 1 + protocols: [2, 3], // required, array of integers + api: [2, 3], // required, array of integers + playbackmethod: [2,6], // required, array of integers + skippable: true, // optional, boolean + skipafter: 10 // optional, integer + } + } + }] +}]; ``` -var adUnits = [ - // Banner adUnit - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - } - bids: [{ - bidder: 'yieldmo', - params: { - placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) - bidFloor: .28 // optional param - } - }] - } -]; -``` \ No newline at end of file diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index caeb26266fee..ae97d474f1d7 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -1,19 +1,16 @@ import { expect } from 'chai'; import { spec } from 'modules/yieldmoBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; describe('YieldmoAdapter', function () { - const adapter = newBidder(spec); - const ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const BANNER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const VIDEO_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; - let tdid = '8d146286-91d4-4958-aff4-7e489dd1abd6'; - let criteoId = 'aff4'; - - let bid = { + const mockBannerBid = (rootParams = {}, params = {}) => ({ bidder: 'yieldmo', params: { bidFloor: 0.1, + ...params, }, adUnitCode: 'adunit-code', mediaTypes: { @@ -31,15 +28,44 @@ describe('YieldmoAdapter', function () { pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', }, userId: { - tdid, + tdid: '8d146286-91d4-4958-aff4-7e489dd1abd6' + }, + ...rootParams + }); + + const mockVideoBid = (rootParams = {}, params = {}, videoParams = {}) => ({ + bidder: 'yieldmo', + adUnitCode: 'adunit-code-video', + bidId: '321video123', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + mimes: ['video/mp4'] + }, }, - }; - let bidArray = [bid]; - let bidderRequest = { + params: { + placementId: '123', + ...params, + video: { + placement: 1, + maxduration: 30, + startdelay: 10, + protocols: [2, 3], + api: [2, 3], + skipppable: true, + playbackmethod: [1, 2], + ...videoParams + } + }, + ...rootParams + }); + + const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ bidderCode: 'yieldmo', auctionId: 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', bidderRequestId: '14c4ede8c693f', - bids: bidArray, + bids, auctionStart: 1520001292880, timeout: 3000, start: 1520001292884, @@ -49,236 +75,215 @@ describe('YieldmoAdapter', function () { reachedTop: true, referer: 'yieldmo.com', }, - }; + ...params + }); describe('isBidRequestValid', function () { - it('should return true when necessary information is found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + describe('Banner:', function () { + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockBannerBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bid + expect(spec.isBidRequestValid({})).to.be.false; + + // empty bidId + expect(spec.isBidRequestValid(mockBannerBid({bidId: ''}))).to.be.false; + + // empty adUnitCode + expect(spec.isBidRequestValid(mockBannerBid({adUnitCode: ''}))).to.be.false; + + let invalidBid = mockBannerBid(); + delete invalidBid.mediaTypes.banner; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); }); - it('should return false when necessary information is not found', function () { - // empty bid - expect(spec.isBidRequestValid({})).to.be.false; + describe('Instream video:', function () { + const getVideoBidWithoutParam = (key, paramToRemove) => { + let bid = mockVideoBid(); + delete utils.deepAccess(bid, key)[paramToRemove]; + return bid; + } - // empty bidId - bid.bidId = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockVideoBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bidId + expect(spec.isBidRequestValid(mockVideoBid({bidId: ''}))).to.be.false; - // empty adUnitCode - bid.bidId = '30b31c1838de1e'; - bid.adUnitCode = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + // empty adUnitCode + expect(spec.isBidRequestValid(mockVideoBid({adUnitCode: ''}))).to.be.false; + }); + + it('should return false when required mediaTypes.video.* param is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('mediaTypes.video', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('playerSize'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('mimes'))).to.be.false; + }); + + it('should return false when required bid.params.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('placementId'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('video'))).to.be.false; + }); - bid.adUnitCode = 'adunit-code'; + it('should return false when required bid.params.video.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params.video', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('placement'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('maxduration'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('startdelay'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('protocols'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('api'))).to.be.false; + }); }); }); describe('buildRequests', function () { - it('should attempt to send bid requests to the endpoint via GET', function () { - const request = spec.buildRequests(bidArray, bidderRequest); - expect(request.method).to.equal('GET'); - expect(request.url).to.be.equal(ENDPOINT); - }); + const build = (bidRequests, bidderReq = mockBidderRequest()) => spec.buildRequests(bidRequests, bidderReq); + const buildAndGetPlacementInfo = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data.p`); + const buildAndGetData = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data`) || {}; - it('should not blow up if crumbs is undefined', function () { - let bidArray = [{ ...bid, crumbs: undefined }]; - expect(function () { - spec.buildRequests(bidArray, bidderRequest); - }).not.to.throw(); - }); + describe('Banner:', function () { + it('should attempt to send banner bid requests to the endpoint via GET', function () { + const requests = build([mockBannerBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('GET'); + expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); + }); - it('should place bid information into the p parameter of data', function () { - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' - ); - bidArray.push({ - bidder: 'yieldmo', - params: { - bidFloor: 0.2, - }, - adUnitCode: 'adunit-code-1', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '123456789', - bidderRequestId: '987654321', - auctionId: '0246810', - crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', - }, + it('should not blow up if crumbs is undefined', function () { + expect(function () { + build([mockBannerBid({crumbs: undefined})]); + }).not.to.throw(); }); - // multiple placements - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' - ); - }); + it('should place bid information into the p parameter of data', function () { + let bidArray = [mockBannerBid()]; + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' + ); - it('should add placement id if given', function () { - bidArray[0].params.placementId = 'ym_1293871298'; - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); + // multiple placements + bidArray.push(mockBannerBid( + {adUnitCode: 'adunit-2', bidId: '123a', bidderRequestId: '321', auctionId: '222'}, {bidFloor: 0.2})); + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},' + + '{"placement_id":"adunit-2","callback_id":"123a","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' + ); + }); - bidArray[1].params.placementId = 'ym_0987654321'; - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); - }); + it('should add placement id if given', function () { + let bidArray = [mockBannerBid({}, {placementId: 'ym_1293871298'})]; + let placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); + + bidArray.push(mockBannerBid({}, {placementId: 'ym_0987654321'})); + placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); + }); - it('should add additional information to data parameter of request', function () { - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.hasOwnProperty('page_url')).to.be.true; - expect(data.hasOwnProperty('bust')).to.be.true; - expect(data.hasOwnProperty('pr')).to.be.true; - expect(data.hasOwnProperty('scrd')).to.be.true; - expect(data.dnt).to.be.false; - expect(data.hasOwnProperty('description')).to.be.true; - expect(data.hasOwnProperty('title')).to.be.true; - expect(data.hasOwnProperty('h')).to.be.true; - expect(data.hasOwnProperty('w')).to.be.true; - expect(data.hasOwnProperty('pubcid')).to.be.true; + it('should add additional information to data parameter of request', function () { + const data = buildAndGetData([mockBannerBid()]); + expect(data.hasOwnProperty('page_url')).to.be.true; + expect(data.hasOwnProperty('bust')).to.be.true; + expect(data.hasOwnProperty('pr')).to.be.true; + expect(data.hasOwnProperty('scrd')).to.be.true; + expect(data.dnt).to.be.false; + expect(data.hasOwnProperty('description')).to.be.true; + expect(data.hasOwnProperty('title')).to.be.true; + expect(data.hasOwnProperty('h')).to.be.true; + expect(data.hasOwnProperty('w')).to.be.true; + expect(data.hasOwnProperty('pubcid')).to.be.true; expect(data.userConsent).to.equal('{"gdprApplies":"","cmp":""}'); expect(data.us_privacy).to.equal(''); }); - it('should add pubcid as parameter of request', function () { - const pubcidBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2', - }, - }; - const data = spec.buildRequests([pubcidBid], bidderRequest).data; - expect(data.pubcid).to.deep.equal( - 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' - ); - }); + it('should add pubcid as parameter of request', function () { + const pubcid = 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2'; + const pubcidBid = mockBannerBid({crumbs: undefined, userId: {pubcid}}); + expect(buildAndGetData([pubcidBid]).pubcid).to.deep.equal(pubcid); + }); - it('should add unified id as parameter of request', function () { - const unifiedIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - tdid, - }, - }; - const data = spec.buildRequests([unifiedIdBid], bidderRequest).data; - expect(data.tdid).to.deep.equal(tdid); - }); + it('should add unified id as parameter of request', function () { + const unifiedIdBid = mockBannerBid({crumbs: undefined}); + expect(buildAndGetData([unifiedIdBid]).tdid).to.deep.equal(mockBannerBid().userId.tdid); + }); - it('should add CRITEO RTUS id as parameter of request', function () { - const criteoIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - criteoId, - }, - }; - const data = spec.buildRequests([criteoIdBid], bidderRequest).data; - expect(data.cri_prebid).to.deep.equal(criteoId); - }); + it('should add CRITEO RTUS id as parameter of request', function () { + const criteoId = 'aff4'; + const criteoIdBid = mockBannerBid({crumbs: undefined, userId: { criteoId }}); + expect(buildAndGetData([criteoIdBid]).cri_prebid).to.deep.equal(criteoId); + }); - it('should add gdpr information to request if available', () => { - bidderRequest.gdprConsent = { - consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - vendorData: { blerp: 1 }, - gdprApplies: true, - }; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.userConsent).equal( - JSON.stringify({ + it('should add gdpr information to request if available', () => { + const gdprConsent = { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {blerp: 1}, gdprApplies: true, - cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - }) - ); - }); + }; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({gdprConsent})); + expect(data.userConsent).equal( + JSON.stringify({ + gdprApplies: true, + cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + }) + ); + }); + + it('should add ccpa information to request if available', () => { + const uspConsent = '1YNY'; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({uspConsent})); + expect(data.us_privacy).equal(uspConsent); + }); - it('should add ccpa information to request if available', () => { - const privacy = '1YNY'; - bidderRequest.uspConsent = privacy; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.us_privacy).equal(privacy); + it('should add schain if it is in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{asi: 'indirectseller.com', sid: '00001', hp: 1}], + }; + const data = buildAndGetData([mockBannerBid({schain})]); + expect(data.schain).equal(JSON.stringify(schain)); + }); }); - it('should add schain if it is in the bidRequest', () => { - const schain = { - ver: '1.0', - complete: 1, - nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], - }; - bidArray[0].schain = schain; - const request = spec.buildRequests([bidArray[0]], bidderRequest); - expect(request.data.schain).equal(JSON.stringify(schain)); + describe('Instream video:', function () { + it('should attempt to send banner bid requests to the endpoint via POST', function () { + const requests = build([mockVideoBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); }); }); describe('interpretResponse', function () { - let serverResponse; - - beforeEach(function () { - serverResponse = { - body: [ - { - callback_id: '21989fdbef550a', - cpm: 3.45455, - width: 300, - height: 250, - ad: - '
', - creative_id: '9874652394875', - }, - ], - header: 'header?', - }; + const mockServerResponse = () => ({ + body: [{ + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: ' ' + + '
', + creative_id: '9874652394875', + }], + header: 'header?', }); it('should correctly reorder the server response', function () { - const newResponse = spec.interpretResponse(serverResponse); + const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ requestId: '21989fdbef550a', @@ -289,19 +294,18 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: - '
', + ad: ' ' + + '
', }); }); it('should not add responses if the cpm is 0 or null', function () { - serverResponse.body[0].cpm = 0; - let response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + let response = mockServerResponse(); + response.body[0].cpm = 0; + expect(spec.interpretResponse(response)).to.deep.equal([]); - serverResponse.body[0].cpm = null; - response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + response.body[0].cpm = null; + expect(spec.interpretResponse(response)).to.deep.equal([]); }); });