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);
+ })
});
});