diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 5b48e732942..ea0ce897395 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -1,4 +1,5 @@ -import { getWindowTop, isFn, logWarn, getDNT, deepAccess, isArray, inIframe, mergeDeep, isStr, isEmpty, deepSetValue, deepClone, parseUrl, cleanObj, logError, triggerPixel } from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; +import { getWindowTop, isFn, logWarn, getDNT, deepAccess, isArray, inIframe, mergeDeep, isStr, isEmpty, deepSetValue, deepClone, parseUrl, cleanObj, logError, triggerPixel, isInteger, isNumber } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -8,10 +9,63 @@ const AUCTION_TYPE = 1; const BIDDER_CODE = 'mediakeys'; const ENDPOINT = 'https://prebid.eu-central-1.bidder.mediakeys.io/bids'; const GVLID = 498; -const SUPPORTED_MEDIA_TYPES = [BANNER]; +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const DEFAULT_CURRENCY = 'USD'; const NET_REVENUE = true; +const NATIVE_ASSETS_MAPPING = [ + { name: 'title', id: 1, type: 0 }, + { name: 'image', id: 2, type: 3 }, + { name: 'icon', id: 3, type: 1 }, + { name: 'sponsoredBy', id: 5, type: 1 }, + { name: 'body', id: 6, type: 2 }, + { name: 'rating', id: 7, type: 3 }, + { name: 'likes', id: 8, type: 4 }, + { name: 'downloads', id: 9, type: 5 }, + { name: 'price', id: 10, type: 6 }, + { name: 'salePrice', id: 11, type: 7 }, + { name: 'phone', id: 12, type: 8 }, + { name: 'address', id: 13, type: 9 }, + { name: 'body2', id: 14, type: 10 }, + { name: 'displayUrl', id: 15, type: 11 }, + { name: 'cta', id: 16, type: 12 }, +]; + +// This provide a whitelist and a basic validation of OpenRTB native 1.2 options. +// https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf +const ORTB_NATIVE_PARAMS = { + context: value => [1, 2, 3].indexOf(value) !== -1, + plcmttype: value => [1, 2, 3, 4].indexOf(value) !== -1 +}; + +// This provide a whitelist and a basic validation of OpenRTB 2.5 video options. +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf +const ORTB_VIDEO_PARAMS = { + mimes: value => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + minduration: value => isInteger(value), + maxduration: value => isInteger(value), + protocols: value => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].indexOf(v) !== -1), + w: value => isInteger(value), + h: value => isInteger(value), + startdelay: value => isInteger(value), + placement: value => [1, 2, 3, 4, 5].indexOf(value) !== -1, + linearity: value => [1, 2].indexOf(value) !== -1, + skip: value => [0, 1].indexOf(value) !== -1, + skipmin: value => isInteger(value), + skipafter: value => isInteger(value), + sequence: value => isInteger(value), + battr: value => Array.isArray(value) && value.every(v => Array.from({length: 17}, (_, i) => i + 1).indexOf(v) !== -1), + maxextended: value => isInteger(value), + minbitrate: value => isInteger(value), + maxbitrate: value => isInteger(value), + boxingallowed: value => [0, 1].indexOf(value) !== -1, + playbackmethod: value => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1), + playbackend: value => [1, 2, 3].indexOf(value) !== -1, + delivery: value => [1, 2, 3].indexOf(value) !== -1, + pos: value => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1, + api: value => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1), +}; + /** * Detects the capability to reach window.top. * @@ -83,6 +137,33 @@ function getFloor(bid, mediaType, size = '*') { return (!isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) ? floor.floor : false } +/** + * Returns the highest floor price found when a bid have + * several mediaTypes. + * + * @param {*} bid a Prebid.js bid (request) object + * @returns {number|boolean} + */ +function getHighestFloor(bid) { + const floors = []; + + for (let mediaType in bid.mediaTypes) { + const floor = getFloor(bid, mediaType); + + if (isNumber(floor)) { + floors.push(floor); + } + } + + if (!floors.length) { + return false; + } + + return floors.reduce((a, b) => { + return Math.max(a, b); + }); +} + /** * Returns an openRTB 2.5 object. * This one will be populated at each step of the buildRequest process. @@ -153,6 +234,190 @@ function createBannerImp(bid) { } } +/** + * Returns an openRtb 2.5 native object with a native 1.2 request. + * + * @param {object} bid Prebid bid object from request + * @returns {object} + */ +function createNativeImp(bid) { + if (!bid.nativeParams) { + logWarn(`${BIDDER_CODE}: bid.nativeParams object has not been found.`); + return + } + + const nativeParams = deepClone(bid.nativeParams); + + const nativeAdUnitParams = deepAccess(bid, 'mediaTypes.native', {}); + const nativeBidderParams = deepAccess(bid, 'params.native', {}); + + const extraParams = { + ...nativeAdUnitParams, + ...nativeBidderParams + }; + + const nativeObject = { + ver: '1.2', + context: 1, // overwrited later if needed + plcmttype: 1, // overwrited later if needed + assets: [] + } + + Object.keys(ORTB_NATIVE_PARAMS).forEach(name => { + if (extraParams.hasOwnProperty(name)) { + if (ORTB_NATIVE_PARAMS[name](extraParams[name])) { + nativeObject[name] = extraParams[name]; + } else { + logWarn(`${BIDDER_CODE}: the OpenRTB native param ${name} has been skipped due to misformating. Please refer to OpenRTB Native spec.`); + } + } + }); + + // just a helper function + const setImageAssetSizes = function(asset, param) { + if (param.sizes && param.sizes.length) { + asset.img.w = param.sizes ? param.sizes[0] : undefined; + asset.img.h = param.sizes ? param.sizes[1] : undefined; + } + + if (!asset.img.w) { + asset.img.wmin = 0; + } + + if (!asset.img.h) { + asset.img.hmin = 0; + } + } + + // Prebid.js "image" type support. + // Add some defaults to support special type provided by Prebid.js `mediaTypes.native.type: "image"` + const nativeImageType = deepAccess(bid, 'mediaTypes.native.type'); + if (nativeImageType === 'image') { + // Default value is ones of the recommended by the spec: https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf + nativeParams.title.len = 90; + } + + for (let key in nativeParams) { + if (nativeParams.hasOwnProperty(key)) { + const internalNativeAsset = find(NATIVE_ASSETS_MAPPING, ref => ref.name === key); + if (!internalNativeAsset) { + logWarn(`${BIDDER_CODE}: the asset "${key}" has not been found in Prebid assets map. Skipped for request.`); + continue; + } + + const param = nativeParams[key]; + + const asset = { + id: internalNativeAsset.id, + required: param.required ? 1 : 0 + } + + switch (key) { + case 'title': + if (param.len || param.length) { + asset.title = { + len: param.len || param.length, + ext: param.ext + } + } else { + logWarn(`${BIDDER_CODE}: "title.length" property for native asset is required. Skipped for request.`) + continue; + } + break; + + case 'image': + asset.img = { + type: internalNativeAsset.type, + mimes: param.mimes, + ext: param.ext, + } + + setImageAssetSizes(asset, param); + + break; + case 'icon': + asset.img = { + type: internalNativeAsset.type, + mimes: param.mimes, + ext: param.ext, + } + + setImageAssetSizes(asset, param); + break; + + case 'sponsoredBy': // sponsored + case 'body': // desc + case 'rating': + case 'likes': + case 'downloads': + case 'price': + case 'salePrice': + case 'phone': + case 'address': + case 'body2': // desc2 + case 'displayUrl': + case 'cta': + // generic asset.data + asset.data = { + type: internalNativeAsset.type, + len: param.len, + ext: param.ext + } + break; + } + + nativeObject.assets.push(asset); + } + } + + if (nativeObject.assets.length) { + return { + request: nativeObject + } + } +} + +/** + * Returns an openRtb 2.5 video object. + * + * @param {object} bid Prebid bid object from request + * @returns {object} + */ +function createVideoImp(bid) { + const videoAdUnitParams = deepAccess(bid, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bid, 'params.video', {}); + const computedParams = {}; + + // Special case for playerSize. + // Eeach props will be overrided if they are defined in config. + if (Array.isArray(videoAdUnitParams.playerSize)) { + const tempSize = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize; + computedParams.w = tempSize[0]; + computedParams.h = tempSize[1]; + } + + const videoParams = { + ...computedParams, + ...videoAdUnitParams, + ...videoBidderParams + }; + + const video = {}; + + // Only whitelisted OpenRTB options need to be validated. + Object.keys(ORTB_VIDEO_PARAMS).forEach(name => { + if (videoParams.hasOwnProperty(name)) { + if (ORTB_VIDEO_PARAMS[name](videoParams[name])) { + video[name] = videoParams[name]; + } else { + logWarn(`${BIDDER_CODE}: the OpenRTB video param ${name} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); + } + } + }); + + return video +} + /** * Create the OpenRTB 2.5 imp object. * @@ -167,20 +432,34 @@ function createImp(bid) { secure: 1, }; + // There is no default floor. bidfloor is set only + // if the priceFloors module is activated and returns a valid floor. + const floor = getHighestFloor(bid); + if (isNumber(floor)) { + imp.bidfloor = floor; + } + // Only supports proper mediaTypes definition… for (let mediaType in bid.mediaTypes) { - // There is no default floor. bidfloor is set only - // if the priceFloors module is activated and returns a valid floor. - const floor = getFloor(bid, mediaType); - if (floor) { - imp.bidfloor = floor; - } - - if (mediaType === BANNER) { - const banner = createBannerImp(bid); - if (banner) { - imp.banner = banner; - } + switch (mediaType) { + case BANNER: + const banner = createBannerImp(bid); + if (banner) { + imp.banner = banner; + } + break; + case NATIVE: + const native = createNativeImp(bid); + if (native) { + imp.native = native; + } + break; + case VIDEO: + const video = createVideoImp(bid); + if (video) { + imp.video = video; + } + break; } } @@ -213,6 +492,94 @@ function getPrimaryCatFromResponse(cat) { } } +/** + * Create the Prebid.js native object from response. + * + * @param {*} bid bid object from response + * @returns {object} Prebid.js native object used in response + */ +function nativeBidResponseHandler(bid) { + const nativeAdm = JSON.parse(bid.adm); + if (!nativeAdm || !nativeAdm.assets.length) { + logError(`${BIDDER_CODE}: invalid native response.`); + return; + } + + const native = {} + + nativeAdm.assets.forEach(asset => { + if (asset.title) { + native.title = asset.title.text; + return; + } + + if (asset.img) { + switch (asset.img.type) { + case 1: + native.icon = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + break; + default: + native.image = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + break; + } + return; + } + + if (asset.data) { + const internalNativeAsset = find(NATIVE_ASSETS_MAPPING, ref => ref.id === asset.id); + if (internalNativeAsset) { + native[internalNativeAsset.name] = asset.data.value; + } + } + }); + + if (nativeAdm.link) { + if (nativeAdm.link.url) { + native.clickUrl = nativeAdm.link.url; + } + if (Array.isArray(nativeAdm.link.clicktrackers)) { + native.clickTrackers = nativeAdm.link.clicktrackers + } + } + + if (Array.isArray(nativeAdm.eventtrackers)) { + native.impressionTrackers = []; + nativeAdm.eventtrackers.forEach(tracker => { + // Only Impression events are supported. Prebid does not support Viewability events yet. + if (tracker.event !== 1) { + return; + } + + // methods: + // 1: image + // 2: js + // note: javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. + switch (tracker.method) { + case 1: + native.impressionTrackers.push(tracker.url); + break; + case 2: + native.javascriptTrackers = ``; + break; + } + }); + } + + if (nativeAdm.privacy) { + native.privacyLink = nativeAdm.privacy; + } + + return native; +} + export const spec = { code: BIDDER_CODE, @@ -355,6 +722,29 @@ export const spec = { meta: cleanObj(meta) }; + if (mediaType === NATIVE) { + const native = nativeBidResponseHandler(bid); + if (native) { + newBid.native = native; + } + } + + if (mediaType === VIDEO) { + // Note: + // Mediakeys bid adapter expects a publisher has set his own video player + // in the `mediaTypes.video` configuration object. + + // Mediakeys bidder does not provide inline XML in the bid response + // newBid.vastXml = bid.ext.vast_url; + + // For instream video, disable server cache as vast is generated per bid request + newBid.videoCacheKey = 'no_cache'; + + // The vast URL is server independently and must be fetched before video rendering in the renderer + // appending '&no_cache' is safe and fast as the vast url always have parameters + newBid.vastUrl = bid.ext.vast_url + '&no_cache'; + } + bidResponses.push(newBid); }); }); diff --git a/modules/mediakeysBidAdapter.md b/modules/mediakeysBidAdapter.md index 75e69659c8a..ec313c2fe3a 100644 --- a/modules/mediakeysBidAdapter.md +++ b/modules/mediakeysBidAdapter.md @@ -29,3 +29,111 @@ var adUnits = [ }] }, ``` + +## Native only Ad Unit + +The Mediakeys adapter accepts two optional params for native requests. Please see the [OpenRTB Native Ads Specification](https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf) for valid values. + +``` +var adUnits = [ +{ + code: 'test', + mediaTypes: { + native: { + type: 'image', + } + }, + bids: [{ + bidder: 'mediakeys', + params: { + native: { + context: 1, // ORTB Native Context Type IDs. Default `1`. + plcmttype: 1, // ORTB Native Placement Type IDs. Default `1`. + } + } + }] +}, +``` + +## Video only Ad Unit + +The Mediakeys adapter accepts any valid openRTB 2.5 video property. Properties can be defined at the adUnit `mediaTypes.video` or `bid[].params` level. + +### Outstream context + +``` +var adUnits = [ +{ + code: 'test', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [300, 250], + // additional OpenRTB video params + // placement: 2, + // api: [1], + // … + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + var bidReqConfig = pbjs.adUnits.find(bidReq => bidReq.bidId === bid.impid); + + if (bidReqConfig && bidReqConfig.mediaTypes && bidReqConfig.mediaTypes.video && bidReqConfig.mediaTypes.video.context === 'outstream') { + var adResponse = fetch(bid.vastUrl).then(resp => resp.text()).then(text => ({ + ad: { + video: { + content: text, + player_width: bid.width || bidReqConfig.mediaTypes.video.playerSize[0], + player_height: bid.height || bidReqConfig.mediaTypes.video.playerSize[1], + } + } + })) + + adResponse.then((ad) => { + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: ad + }); + }); + }) + } + } + }, + bids: [{ + bidder: 'mediakeys', + params: { + video: { + // additional OpenRTB video params. Will be merged with params defined at mediaTypes level + } + } + }] +}, +``` + +### Instream context + +``` +var adUnits = [ +{ + code: 'test', + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 250], + // additional OpenRTB video params + // placement: 2, + // api: [1], + // … + } + }, + bids: [{ + bidder: 'mediakeys', + params: { + // additional OpenRTB video params. Will be merged with params defined at mediaTypes level + } + }] +}, +``` diff --git a/test/spec/modules/mediakeysBidAdapter_spec.js b/test/spec/modules/mediakeysBidAdapter_spec.js index 040c0abd566..602524e6eb3 100644 --- a/test/spec/modules/mediakeysBidAdapter_spec.js +++ b/test/spec/modules/mediakeysBidAdapter_spec.js @@ -1,9 +1,11 @@ import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; import { spec } from 'modules/mediakeysBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; +import { OUTSTREAM } from '../../../src/video.js'; describe('mediakeysBidAdapter', function () { const adapter = newBidder(spec); @@ -39,39 +41,100 @@ describe('mediakeysBidAdapter', function () { } }; + const bidNative = { + bidder: 'mediakeys', + params: {}, + mediaTypes: { + native: { + body: { + required: true + }, + title: { + required: true, + len: 800 + }, + sponsoredBy: { + required: true + }, + body2: { + required: true + }, + image: { + required: true, + sizes: [[300, 250], [300, 600], [100, 150]], + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + }, + nativeParams: { + body: { + required: true + }, + title: { + required: true, + len: 800 + }, + sponsoredBy: { + required: true + }, + body2: { + required: true + }, + image: { + required: true, + sizes: [[300, 250], [300, 600], [100, 150]], + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '47789656-9e5c-4250-b7e0-2ce4cbe71a55', + bidId: '299320f4de980d', + bidderRequestId: '1c1b642f803242', + auctionId: '84212956-c377-40e8-b000-9885a06dc692', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2Imp: { + ext: { data: { something: 'test' } } + } + }; + + const bidVideo = { + bidder: 'mediakeys', + params: {}, + mediaTypes: { + video: { + context: OUTSTREAM, + playerSize: [480, 320] + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '47789656-9e5c-4250-b7e0-2ce4cbe71a55', + bidId: '299320f4de980d', + bidderRequestId: '1c1b642f803242', + auctionId: '84212956-c377-40e8-b000-9885a06dc692', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2Imp: { + ext: { data: { something: 'test' } } + } + }; + const bidderRequest = { bidderCode: 'mediakeys', auctionId: '84212956-c377-40e8-b000-9885a06dc692', bidderRequestId: '1c1b642f803242', bids: [ - { - bidder: 'mediakeys', - params: {}, - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - adUnitCode: 'div-gpt-ad-1460505748561-0', - transactionId: '47789656-9e5c-4250-b7e0-2ce4cbe71a55', - sizes: [ - [300, 250], - [300, 600], - ], - bidId: '299320f4de980d', - bidderRequestId: '1c1b642f803242', - auctionId: '84212956-c377-40e8-b000-9885a06dc692', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0, - ortb2Imp: { - ext: { data: { something: 'test' } } - } - }, + bid ], auctionStart: 1620973766319, timeout: 1000, @@ -116,23 +179,25 @@ describe('mediakeysBidAdapter', function () { it('should create imp for supported mediaType only', function() { const bidRequests = [utils.deepClone(bid)]; const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; bidRequests[0].mediaTypes.video = { playerSize: [300, 250], - context: 'outstream' + context: OUTSTREAM } - bidRequests[0].mediaTypes.native = { - type: 'image' - } + bidRequests[0].mediaTypes.native = bidNative.mediaTypes.native; + bidRequests[0].nativeParams = bidNative.mediaTypes.native; + + bidderRequestCopy.bids = bidRequests[0]; const request = spec.buildRequests(bidRequests, bidderRequestCopy); const data = request.data; expect(data.imp.length).to.equal(1); expect(data.imp[0].banner).to.exist; - expect(data.imp[0].video).to.not.exist; - expect(data.imp[0].native).to.not.exist; + expect(data.imp[0].video).to.exist; + expect(data.imp[0].native).to.exist; }); it('should get expected properties with default values (no params set)', function () { @@ -161,6 +226,205 @@ describe('mediakeysBidAdapter', function () { expect(data.imp[0].ext.data.something).to.equal('test'); }); + describe('native imp', function() { + it('should get a native object in request', function() { + const bidRequests = [utils.deepClone(bidNative)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].native).to.exist; + expect(data.imp[0].native.request.ver).to.equal('1.2'); + expect(data.imp[0].native.request.context).to.equal(1); + expect(data.imp[0].native.request.plcmttype).to.equal(1); + expect(data.imp[0].native.request.assets.length).to.equal(6); + // find the asset body + const bodyAsset = find(data.imp[0].native.request.assets, asset => asset.id === 6); + expect(bodyAsset.data.type).to.equal(2); + }); + + it('should get a native object in request with properties filled with params values', function() { + const bidRequests = [utils.deepClone(bidNative)]; + bidRequests[0].params = { + native: { + context: 3, + plcmttype: 3, + } + } + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].native).to.exist; + expect(data.imp[0].native.request.ver).to.equal('1.2'); + expect(data.imp[0].native.request.context).to.equal(3); + expect(data.imp[0].native.request.plcmttype).to.equal(3); + expect(data.imp[0].native.request.assets.length).to.equal(6); + }); + + it('should get a native object in request when native type ,image" has been set', function() { + const bidRequests = [utils.deepClone(bidNative)]; + bidRequests[0].mediaTypes.native = { type: 'image' }; + bidRequests[0].nativeParams = { + image: { required: true }, + title: { required: true }, + sponsoredBy: { required: true }, + clickUrl: { required: true }, // [1] Will be ignored as it is used in response validation only + body: { required: false }, + icon: { required: false }, + }; + + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].native).to.exist; + expect(data.imp[0].native.request.ver).to.equal('1.2'); + expect(data.imp[0].native.request.context).to.equal(1); + expect(data.imp[0].native.request.plcmttype).to.equal(1); + expect(data.imp[0].native.request.assets.length).to.equal(5); // [1] clickUrl ignored + }); + + it('should log errors and ignore misformated assets', function() { + const bidRequests = [utils.deepClone(bidNative)]; + delete bidRequests[0].nativeParams.title.len; + bidRequests[0].nativeParams.unregistred = {required: true}; + + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + utilsMock.expects('logWarn').twice(); + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].native).to.exist; + expect(data.imp[0].native.request.assets.length).to.equal(5); + }); + }); + + describe('video imp', function() { + it('should get a video object in request', function() { + const bidRequests = [utils.deepClone(bidVideo)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].banner).to.not.exist; + expect(data.imp[0].video).to.exist; + expect(data.imp[0].video.w).to.equal(480); + expect(data.imp[0].video.h).to.equal(320); + }); + + it('should ignore and warn misformated ORTB video properties', function() { + const bidRequests = [utils.deepClone(bidVideo)]; + bidRequests[0].mediaTypes.video.unknown = 'foo'; + bidRequests[0].mediaTypes.video.placement = 10; + bidRequests[0].mediaTypes.video.skipmin = 5; + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].banner).to.not.exist; + expect(data.imp[0].video).to.exist; + expect(data.imp[0].video.w).to.equal(480); + expect(data.imp[0].video.h).to.equal(320); + expect(data.imp[0].video.skipmin).to.equal(5); + expect(data.imp[0].video.placement).to.not.exist; + expect(data.imp[0].video.unknown).to.not.exist; + }); + + it('should merge adUnit mediaTypes level and bidder level params properties ', function() { + const bidRequests = [utils.deepClone(bidVideo)]; + bidRequests[0].mediaTypes.video.placement = 1; + bidRequests[0].mediaTypes.video.mimes = ['video/mpeg4']; + bidRequests[0].mediaTypes.video.protocols = [1]; + bidRequests[0].mediaTypes.video.minduration = 10; + bidRequests[0].mediaTypes.video.maxduration = 45; + bidRequests[0].mediaTypes.video.skipmin = 5; + bidRequests[0].mediaTypes.video.sequence = 3; + bidRequests[0].mediaTypes.video.linearity = 1; + bidRequests[0].mediaTypes.video.battr = [12]; + bidRequests[0].mediaTypes.video.maxextended = 10; + bidRequests[0].mediaTypes.video.minbitrate = 720; + bidRequests[0].mediaTypes.video.maxbitrate = 720; + bidRequests[0].mediaTypes.video.boxingallowed = 1; + bidRequests[0].mediaTypes.video.playbackmethod = [1]; + bidRequests[0].mediaTypes.video.playbackend = 2; + bidRequests[0].mediaTypes.video.delivery = 2; + bidRequests[0].mediaTypes.video.pos = 0; + bidRequests[0].mediaTypes.video.companionad = [{ w: 360, h: 80 }] + bidRequests[0].mediaTypes.video.api = [1]; + bidRequests[0].mediaTypes.video.companiontype = [1]; + + // bidder level + bidRequests[0].params.video = { + pos: 2, // override + skip: 1, + skipafter: 10, + startdelay: 3 + }; + + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + utilsMock.expects('logWarn').never(); + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = request.data; + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].banner).to.not.exist; + expect(data.imp[0].video).to.exist; + + expect(Object.keys(data.imp[0].video).length).to.equal(23); // 21 ortb params (2 skipped) + computed width/height. + expect(data.imp[0].video.w).to.equal(480); + expect(data.imp[0].video.h).to.equal(320); + expect(data.imp[0].video.mimes[0]).to.equal('video/mpeg4'); + expect(data.imp[0].video.pos).to.equal(2); + expect(data.imp[0].video.skip).to.equal(1); + expect(data.imp[0].video.skipafter).to.equal(10); + expect(data.imp[0].video.startdelay).to.equal(3); + expect(data.imp[0].video.companionad).to.not.exist; + expect(data.imp[0].video.companiontype).to.not.exist; + }); + + it('should log warn message when OpenRTB validation fails ', function() { + const bidRequests = [utils.deepClone(bidVideo)]; + bidRequests[0].mediaTypes.video.placement = 'string'; + bidRequests[0].mediaTypes.video.api = 1; + const bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + utilsMock.expects('logWarn').twice(); + + spec.buildRequests(bidRequests, bidderRequestCopy); + }); + }); + it('should get expected properties with values from params', function () { const bidRequests = [utils.deepClone(bid)]; bidRequests[0].params = { @@ -265,13 +529,25 @@ describe('mediakeysBidAdapter', function () { expect(data.imp[0].bidfloor).to.not.exist; }); - it('should get and set floor by mediatype', function() { + it('should get the highest floorPrice found when bid have several mediaTypes', function() { const bidWithPriceFloors = utils.deepClone(bid); bidWithPriceFloors.mediaTypes.video = { playerSize: [600, 480] }; + bidWithPriceFloors.mediaTypes.native = { + body: { + required: true + } + }; + + bidWithPriceFloors.nativeParams = { + body: { + required: true + } + }; + bidWithPriceFloors.getFloor = getFloorTest; const bidRequests = [bidWithPriceFloors]; @@ -279,10 +555,9 @@ describe('mediakeysBidAdapter', function () { const data = request.data; expect(data.imp[0].banner).to.exist; - expect(data.imp[0].bidfloor).to.equal(1); - - // expect(data.imp[1].video).to.exist; - // expect(data.imp[1].bidfloor).to.equal(5); + expect(data.imp[0].video).to.exist; + expect(data.imp[0].native).to.exist; + expect(data.imp[0].bidfloor).to.equal(5); }); it('should set properties at payload level from FPD', function() { @@ -420,8 +695,8 @@ describe('mediakeysBidAdapter', function () { const bidRequests = [utils.deepClone(bid)]; const request = spec.buildRequests(bidRequests, bidderRequest); sinon.stub(utils, 'isArray').throws(); - spec.interpretResponse(rawServerResponse, request); utilsMock.expects('logError').once(); + spec.interpretResponse(rawServerResponse, request); utils.isArray.restore(); }); @@ -483,28 +758,108 @@ describe('mediakeysBidAdapter', function () { }); }); - it('Build video response', function () { - const bidRequests = [utils.deepClone(bid)]; + it('interprets video bid response', function () { + const vastUrl = 'https://url.local?req=content'; + const bidRequests = [utils.deepClone(bidVideo)]; const request = spec.buildRequests(bidRequests, bidderRequest); + const rawServerResponseVideo = utils.deepClone(rawServerResponse); rawServerResponseVideo.body.seatbid[0].bid[0].ext.prebid.type = 'V'; + rawServerResponseVideo.body.seatbid[0].bid[0].ext.vast_url = vastUrl; + const response = spec.interpretResponse(rawServerResponseVideo, request); expect(response.length).to.equal(1); expect(response[0].mediaType).to.equal('video'); expect(response[0].meta.mediaType).to.equal('video'); + expect(response[0].vastXml).to.not.exist; + expect(response[0].vastUrl).to.equal(vastUrl + '&no_cache'); + expect(response[0].videoCacheKey).to.equal('no_cache'); }); - it('Build native response', function () { - const bidRequests = [utils.deepClone(bid)]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const rawServerResponseVideo = utils.deepClone(rawServerResponse); - rawServerResponseVideo.body.seatbid[0].bid[0].ext.prebid.type = 'N'; - const response = spec.interpretResponse(rawServerResponseVideo, request); + describe('Native response', function () { + let bidRequests; + let bidderRequestCopy; + let request; + let rawServerResponseNative; + let nativeObject; + + beforeEach(function() { + bidRequests = [utils.deepClone(bidNative)]; + bidderRequestCopy = utils.deepClone(bidderRequest); + bidderRequestCopy.bids = bidRequests; + + request = spec.buildRequests(bidRequests, bidderRequestCopy); + + nativeObject = { + ver: '1.2', + privacy: 'https://privacy.me', + assets: [ + { id: 5, data: { type: 1, value: 'Sponsor Brand' } }, + { id: 6, data: { type: 2, value: 'Brand Body' } }, + { id: 14, data: { type: 10, value: 'Brand Body 2' } }, + { id: 1, title: { text: 'Brand Title' } }, + { id: 2, img: { type: 3, url: 'https://url.com/img.jpg', w: 300, h: 250 } }, + { id: 3, img: { type: 1, url: 'https://url.com/ico.png', w: 50, h: 50 } }, + ], + link: { + url: 'https://brand.me', + clicktrackers: [ + 'https://click.me' + ] + }, + eventtrackers: [ + { event: 1, method: 1, url: 'https://click.me' }, + { event: 1, method: 2, url: 'https://click-script.me' } + ] + }; - expect(response.length).to.equal(1); - expect(response[0].mediaType).to.equal('native'); - expect(response[0].meta.mediaType).to.equal('native'); + rawServerResponseNative = utils.deepClone(rawServerResponse); + rawServerResponseNative.body.seatbid[0].bid[0].ext.prebid.type = 'N'; + rawServerResponseNative.body.seatbid[0].bid[0].adm = JSON.stringify(nativeObject) + }); + + it('should ignore invalid native response', function() { + const nativeObjectCopy = utils.deepClone(nativeObject); + nativeObjectCopy.assets = []; + const rawServerResponseNativeCopy = utils.deepClone(rawServerResponseNative); + rawServerResponseNativeCopy.body.seatbid[0].bid[0].adm = JSON.stringify(nativeObjectCopy) + const response = spec.interpretResponse(rawServerResponseNativeCopy, request); + expect(response.length).to.equal(1); + expect(response[0].native).to.not.exist; + }); + + it('should build a classic Prebid.js native object for response', function() { + const rawServerResponseNativeCopy = utils.deepClone(rawServerResponseNative); + const response = spec.interpretResponse(rawServerResponseNativeCopy, request); + expect(response.length).to.equal(1); + expect(response[0].mediaType).to.equal('native'); + expect(response[0].meta.mediaType).to.equal('native'); + expect(response[0].native).to.exist; + expect(response[0].native.body).to.exist; + expect(response[0].native.privacyLink).to.exist; + expect(response[0].native.body2).to.exist; + expect(response[0].native.sponsoredBy).to.exist; + expect(response[0].native.image).to.exist; + expect(response[0].native.icon).to.exist; + expect(response[0].native.title).to.exist; + expect(response[0].native.clickUrl).to.exist; + expect(response[0].native.clickTrackers).to.exist; + expect(response[0].native.clickTrackers.length).to.equal(1); + expect(response[0].native.javascriptTrackers).to.equal(''); + expect(response[0].native.impressionTrackers).to.exist; + expect(response[0].native.impressionTrackers.length).to.equal(1); + }); + + it('should ignore eventtrackers with a unsupported type', function() { + const rawServerResponseNativeCopy = utils.deepClone(rawServerResponseNative); + const nativeObjectCopy = utils.deepClone(nativeObject); + nativeObjectCopy.eventtrackers[0].event = 2; + rawServerResponseNativeCopy.body.seatbid[0].bid[0].adm = JSON.stringify(nativeObjectCopy); + const response = spec.interpretResponse(rawServerResponseNativeCopy, request); + expect(response[0].native.impressionTrackers).to.exist; + expect(response[0].native.impressionTrackers.length).to.equal(0); + }) }); });