diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js
new file mode 100644
index 00000000000..0acdd6406cb
--- /dev/null
+++ b/modules/kulturemediaBidAdapter.js
@@ -0,0 +1,472 @@
+import {
+ deepSetValue,
+ logInfo,
+ deepAccess,
+ logError,
+ isFn,
+ isPlainObject,
+ isStr,
+ isNumber,
+ isArray, logMessage
+} from '../src/utils.js';
+import {registerBidder} from '../src/adapters/bidderFactory.js';
+import {BANNER, VIDEO} from '../src/mediaTypes.js';
+
+const BIDDER_CODE = 'kulturemedia';
+const DEFAULT_BID_TTL = 300;
+const DEFAULT_CURRENCY = 'USD';
+const DEFAULT_NET_REVENUE = true;
+const DEFAULT_NETWORK_ID = 1;
+const OPENRTB_VIDEO_PARAMS = [
+ 'mimes',
+ 'minduration',
+ 'maxduration',
+ 'placement',
+ 'protocols',
+ 'startdelay',
+ 'skip',
+ 'skipafter',
+ 'minbitrate',
+ 'maxbitrate',
+ 'delivery',
+ 'playbackmethod',
+ 'api',
+ 'linearity'
+];
+
+export const spec = {
+ code: BIDDER_CODE,
+ VERSION: '1.0.0',
+ supportedMediaTypes: [BANNER, VIDEO],
+ ENDPOINT: 'https://ads.kulture.media/pbjs',
+
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {BidRequest} bidRequest The bid params to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function (bid) {
+ return (
+ _validateParams(bid) &&
+ _validateBanner(bid) &&
+ _validateVideo(bid)
+ );
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} validBidRequests 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 (validBidRequests, bidderRequest) {
+ if (!validBidRequests || !bidderRequest) {
+ return;
+ }
+
+ // We need to refactor this to support mixed content when there are both
+ // banner and video bid requests
+ let openrtbRequest;
+ if (hasBannerMediaType(validBidRequests[0])) {
+ openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest);
+ } else if (hasVideoMediaType(validBidRequests[0])) {
+ openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest);
+ }
+
+ // adding schain object
+ if (validBidRequests[0].schain) {
+ deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain);
+ }
+
+ // Attaching GDPR Consent Params
+ if (bidderRequest.gdprConsent) {
+ deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
+ deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0));
+ }
+
+ // CCPA
+ if (bidderRequest.uspConsent) {
+ deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent);
+ }
+
+ // EIDS
+ const eids = deepAccess(validBidRequests[0], 'userIdAsEids');
+ if (Array.isArray(eids) && eids.length > 0) {
+ deepSetValue(openrtbRequest, 'user.ext.eids', eids);
+ }
+
+ let publisherId = validBidRequests[0].params.publisherId;
+ let placementId = validBidRequests[0].params.placementId;
+ const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID;
+
+ if (validBidRequests[0].params.e2etest) {
+ logMessage('E2E test mode enabled');
+ publisherId = 'e2etest'
+ }
+ let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId;
+
+ if (placementId) {
+ baseEndpoint += '&placementId=' + placementId
+ }
+ if (networkId) {
+ baseEndpoint += '&nId=' + networkId
+ }
+
+ const payloadString = JSON.stringify(openrtbRequest);
+ return {
+ method: 'POST',
+ url: baseEndpoint,
+ data: payloadString,
+ };
+ },
+
+ interpretResponse: function (serverResponse) {
+ const bidResponses = [];
+ const response = (serverResponse || {}).body;
+ // response is always one seat (exchange) with (optional) bids for each impression
+ if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) {
+ response.seatbid[0].bid.forEach(bid => {
+ if (bid.adm && bid.price) {
+ bidResponses.push(_createBidResponse(bid));
+ }
+ })
+ } else {
+ logInfo('kulturemedia.interpretResponse :: no valid responses to interpret');
+ }
+ return bidResponses;
+ },
+
+ getUserSyncs: function (syncOptions, serverResponses) {
+ logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses);
+ let syncs = [];
+
+ if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
+ return syncs;
+ }
+
+ serverResponses.forEach(resp => {
+ const userSync = deepAccess(resp, 'body.ext.usersync');
+ if (userSync) {
+ let syncDetails = [];
+ Object.keys(userSync).forEach(key => {
+ const value = userSync[key];
+ if (value.syncs && value.syncs.length) {
+ syncDetails = syncDetails.concat(value.syncs);
+ }
+ });
+ syncDetails.forEach(syncDetails => {
+ syncs.push({
+ type: syncDetails.type === 'iframe' ? 'iframe' : 'image',
+ url: syncDetails.url
+ });
+ });
+
+ if (!syncOptions.iframeEnabled) {
+ syncs = syncs.filter(s => s.type !== 'iframe')
+ }
+ if (!syncOptions.pixelEnabled) {
+ syncs = syncs.filter(s => s.type !== 'image')
+ }
+ }
+ });
+ logInfo('kulturemedia.getUserSyncs result=%o', syncs);
+ return syncs;
+ },
+
+};
+
+/* =======================================
+ * Util Functions
+ *======================================= */
+
+/**
+ * @param {BidRequest} bidRequest bid request
+ */
+function hasBannerMediaType(bidRequest) {
+ return !!deepAccess(bidRequest, 'mediaTypes.banner');
+}
+
+/**
+ * @param {BidRequest} bidRequest bid request
+ */
+function hasVideoMediaType(bidRequest) {
+ return !!deepAccess(bidRequest, 'mediaTypes.video');
+}
+
+function _validateParams(bidRequest) {
+ if (!bidRequest.params) {
+ return false;
+ }
+
+ if (bidRequest.params.e2etest) {
+ return true;
+ }
+
+ if (!bidRequest.params.publisherId) {
+ logError('Validation failed: publisherId not declared');
+ return false;
+ }
+
+ if (!bidRequest.params.placementId) {
+ logError('Validation failed: placementId not declared');
+ return false;
+ }
+
+ const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest);
+ if (!mediaTypesExists) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Validates banner bid request. If it is not banner media type returns true.
+ * @param {object} bid, bid to validate
+ * @return boolean, true if valid, otherwise false
+ */
+function _validateBanner(bidRequest) {
+ // If there's no banner no need to validate
+ if (!hasBannerMediaType(bidRequest)) {
+ return true;
+ }
+ const banner = deepAccess(bidRequest, 'mediaTypes.banner');
+ if (!Array.isArray(banner.sizes)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Validates video bid request. If it is not video media type returns true.
+ * @param {object} bid, bid to validate
+ * @return boolean, true if valid, otherwise false
+ */
+function _validateVideo(bidRequest) {
+ // If there's no video no need to validate
+ if (!hasVideoMediaType(bidRequest)) {
+ return true;
+ }
+
+ const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {});
+ const videoBidderParams = deepAccess(bidRequest, 'params.video', {});
+ const params = deepAccess(bidRequest, 'params', {});
+
+ if (params && params.e2etest) {
+ return true;
+ }
+
+ const videoParams = {
+ ...videoPlacement,
+ ...videoBidderParams // Bidder Specific overrides
+ };
+
+ if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) {
+ logError('Validation failed: mimes are invalid');
+ return false;
+ }
+
+ if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) {
+ logError('Validation failed: protocols are invalid');
+ return false;
+ }
+
+ if (!videoParams.context) {
+ logError('Validation failed: context id not declared');
+ return false;
+ }
+
+ if (videoParams.context !== 'instream') {
+ logError('Validation failed: only context instream is supported ');
+ return false;
+ }
+
+ if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) {
+ logError('Validation failed: player size not declared or is not in format [[w,h]]');
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Prepares video request data.
+ *
+ * @param bidRequest
+ * @param bidderRequest
+ * @returns openrtbRequest
+ */
+function buildVideoRequestData(bidRequest, bidderRequest) {
+ const {params} = bidRequest;
+
+ const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {});
+ const videoBidderParams = deepAccess(bidRequest, 'params.video', {});
+
+ const videoParams = {
+ ...videoAdUnit,
+ ...videoBidderParams // Bidder Specific overrides
+ };
+
+ if (bidRequest.params && bidRequest.params.e2etest) {
+ videoParams.playerSize = [[640, 480]]
+ videoParams.conext = 'instream'
+ }
+
+ const video = {
+ w: parseInt(videoParams.playerSize[0][0], 10),
+ h: parseInt(videoParams.playerSize[0][1], 10),
+ }
+
+ // Obtain all ORTB params related video from Ad Unit
+ OPENRTB_VIDEO_PARAMS.forEach((param) => {
+ if (videoParams.hasOwnProperty(param)) {
+ video[param] = videoParams[param];
+ }
+ });
+
+ // Placement Inference Rules:
+ // - If no placement is defined then default to 1 (In Stream)
+ video.placement = video.placement || 2;
+
+ // - If product is instream (for instream context) then override placement to 1
+ if (params.context === 'instream') {
+ video.startdelay = video.startdelay || 0;
+ video.placement = 1;
+ }
+
+ // bid floor
+ const bidFloorRequest = {
+ currency: bidRequest.params.cur || 'USD',
+ mediaType: 'video',
+ size: '*'
+ };
+ let floorData = bidRequest.params
+ if (isFn(bidRequest.getFloor)) {
+ floorData = bidRequest.getFloor(bidFloorRequest);
+ } else {
+ if (params.bidfloor) {
+ floorData = {floor: params.bidfloor, currency: params.currency || 'USD'};
+ }
+ }
+
+ const openrtbRequest = {
+ id: bidRequest.bidId,
+ imp: [
+ {
+ id: '1',
+ video: video,
+ secure: isSecure() ? 1 : 0,
+ bidfloor: floorData.floor,
+ bidfloorcur: floorData.currency
+ }
+ ],
+ site: {
+ domain: bidderRequest.refererInfo.domain,
+ page: bidderRequest.refererInfo.page,
+ ref: bidderRequest.refererInfo.ref,
+ },
+ ext: {
+ hb: 1,
+ prebidver: '$prebid.version$',
+ adapterver: spec.VERSION,
+ },
+ };
+
+ // content
+ if (videoParams.content && isPlainObject(videoParams.content)) {
+ openrtbRequest.site.content = {};
+ const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url'];
+ const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len'];
+ const contentArrayKeys = ['cat'];
+ const contentObjectKeys = ['ext'];
+ for (const contentKey in videoBidderParams.content) {
+ if (
+ (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) ||
+ (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) ||
+ (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) ||
+ (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) &&
+ videoParams.content[contentKey].every(catStr => isStr(catStr)))) {
+ openrtbRequest.site.content[contentKey] = videoParams.content[contentKey];
+ } else {
+ logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined');
+ }
+ }
+ }
+
+ return openrtbRequest;
+}
+
+/**
+ * Prepares video request data.
+ *
+ * @param bidRequest
+ * @param bidderRequest
+ * @returns openrtbRequest
+ */
+function buildBannerRequestData(bidRequests, bidderRequest) {
+ const impr = bidRequests.map(bidRequest => ({
+ id: bidRequest.bidId,
+ banner: {
+ format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({
+ w: sizeArr[0],
+ h: sizeArr[1]
+ }))
+ },
+ ext: {
+ exchange: {
+ placementId: bidRequest.params.placementId
+ }
+ }
+ }));
+
+ const openrtbRequest = {
+ id: bidderRequest.auctionId,
+ imp: impr,
+ site: {
+ domain: bidderRequest.refererInfo?.domain,
+ page: bidderRequest.refererInfo?.page,
+ ref: bidderRequest.refererInfo?.ref,
+ },
+ ext: {}
+ };
+ return openrtbRequest;
+}
+
+function _createBidResponse(bid) {
+ const isADomainPresent =
+ bid.adomain && bid.adomain.length;
+ const bidResponse = {
+ requestId: bid.impid,
+ bidderCode: spec.code,
+ cpm: bid.price,
+ width: bid.w,
+ height: bid.h,
+ ad: bid.adm,
+ ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL,
+ creativeId: bid.crid,
+ netRevenue: DEFAULT_NET_REVENUE,
+ currency: DEFAULT_CURRENCY,
+ mediaType: deepAccess(bid, 'ext.prebid.type', BANNER)
+ }
+
+ if (isADomainPresent) {
+ bidResponse.meta = {
+ advertiserDomains: bid.adomain
+ };
+ }
+
+ if (bidResponse.mediaType === VIDEO) {
+ bidResponse.vastXml = bid.adm;
+ }
+
+ return bidResponse;
+}
+
+function isSecure() {
+ return document.location.protocol === 'https:';
+}
+
+registerBidder(spec);
diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md
new file mode 100644
index 00000000000..0bd17e97982
--- /dev/null
+++ b/modules/kulturemediaBidAdapter.md
@@ -0,0 +1,140 @@
+# Overview
+
+```
+Module Name: Kulture Media Bid Adapter
+Module Type: Bidder Adapter
+Maintainer: devops@kulture.media
+```
+
+# Description
+
+Module that connects to Kulture Media's demand sources.
+Kulture Media bid adapter supports Banner and Video.
+
+
+# Test Parameters
+
+## Banner
+
+```
+var adUnits = [
+ {
+ code: 'banner-ad-div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]]
+ }
+ },
+ bids: [{
+ bidder: 'kulturemedia',
+ params: {
+ placementId: 'test',
+ publisherId: 'test',
+ }
+ }]
+ }
+];
+```
+
+## Video
+
+We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video`
+- 'mimes',
+- 'minduration',
+- 'maxduration',
+- 'placement',
+- 'protocols',
+- 'startdelay',
+- 'skip',
+- 'skipafter',
+- 'minbitrate',
+- 'maxbitrate',
+- 'delivery',
+- 'playbackmethod',
+- 'api',
+- 'linearity'
+
+
+## Instream Video adUnit using mediaTypes.video
+*Note:* By default, the adapter will read the mandatory parameters from mediaTypes.video.
+*Note:* The Video SSP ad server will respond with an VAST XML to load into your defined player.
+```
+ var adUnits = [
+ {
+ code: 'video1',
+ mediaTypes: {
+ video: {
+ context: 'instream',
+ playerSize: [640, 480],
+ mimes: ['video/mp4', 'application/javascript'],
+ protocols: [2,5],
+ api: [2],
+ position: 1,
+ delivery: [2],
+ minduration: 10,
+ maxduration: 30,
+ placement: 1,
+ playbackmethod: [1,5],
+ }
+ },
+ bids: [
+ {
+ bidder: 'kulturemedia',
+ params: {
+ bidfloor: 0.5,
+ publisherId: '12345',
+ placementId: '6789'
+ }
+ }
+ ]
+ }
+ ]
+```
+
+# End To End testing mode
+By passing bid.params.e2etest = true you will be able to receive a test creative
+
+## Banner
+```
+var adUnits = [
+ {
+ code: 'banner-ad-div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]]
+ }
+ },
+ bids: [{
+ bidder: 'kulturemedia',
+ params: {
+ e2etest: true
+ }
+ }]
+ }
+];
+```
+
+## Video
+```
+var adUnits = [
+ {
+ code: 'video1',
+ mediaTypes: {
+ video: {
+ context: "instream",
+ playerSize: [[640, 480]],
+ mimes: ['video/mp4'],
+ protocols: [2,5],
+ }
+ },
+ bids: [
+ {
+ bidder: 'kulturemedia',
+ params: {
+ e2etest: true
+ }
+ }
+ ]
+ }
+]
+```
diff --git a/test/spec/modules/kulturemediaBidAdapter_spec.js b/test/spec/modules/kulturemediaBidAdapter_spec.js
new file mode 100644
index 00000000000..1872f6c171a
--- /dev/null
+++ b/test/spec/modules/kulturemediaBidAdapter_spec.js
@@ -0,0 +1,613 @@
+import {expect} from 'chai';
+import {spec} from 'modules/kulturemediaBidAdapter.js';
+
+const BANNER_REQUEST = {
+ 'bidderCode': 'kulturemedia',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708',
+ 'bidderRequestId': 'requestId',
+ 'bidRequest': [{
+ 'bidder': 'kulturemedia',
+ 'params': {
+ 'placementId': 123456,
+ },
+ 'placementCode': 'div-gpt-dummy-placement-code',
+ 'mediaTypes': {'banner': {'sizes': [[300, 250]]}},
+ 'bidId': 'bidId1',
+ 'bidderRequestId': 'bidderRequestId',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708'
+ },
+ {
+ 'bidder': 'kulturemedia',
+ 'params': {
+ 'placementId': 123456,
+ },
+ 'placementCode': 'div-gpt-dummy-placement-code',
+ 'mediaTypes': {'banner': {'sizes': [[300, 250]]}},
+ 'bidId': 'bidId2',
+ 'bidderRequestId': 'bidderRequestId',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708'
+ }],
+ 'start': 1487883186070,
+ 'auctionStart': 1487883186069,
+ 'timeout': 3000
+};
+
+const RESPONSE = {
+ 'headers': null,
+ 'body': {
+ 'id': 'responseId',
+ 'seatbid': [
+ {
+ 'bid': [
+ {
+ 'id': 'bidId1',
+ 'impid': 'bidId1',
+ 'price': 0.18,
+ 'adm': '',
+ 'adid': '144762342',
+ 'adomain': [
+ 'https://dummydomain.com'
+ ],
+ 'iurl': 'iurl',
+ 'cid': '109',
+ 'crid': 'creativeId',
+ 'cat': [],
+ 'w': 300,
+ 'h': 250,
+ 'ext': {
+ 'prebid': {
+ 'type': 'banner'
+ },
+ 'bidder': {
+ 'appnexus': {
+ 'brand_id': 334553,
+ 'auction_id': 514667951122925701,
+ 'bidder_id': 2,
+ 'bid_ad_type': 0
+ }
+ }
+ }
+ },
+ {
+ 'id': 'bidId2',
+ 'impid': 'bidId2',
+ 'price': 0.1,
+ 'adm': '',
+ 'adid': '144762342',
+ 'adomain': [
+ 'https://dummydomain.com'
+ ],
+ 'iurl': 'iurl',
+ 'cid': '109',
+ 'crid': 'creativeId',
+ 'cat': [],
+ 'w': 300,
+ 'h': 250,
+ 'ext': {
+ 'prebid': {
+ 'type': 'banner'
+ },
+ 'bidder': {
+ 'appnexus': {
+ 'brand_id': 386046,
+ 'auction_id': 517067951122925501,
+ 'bidder_id': 2,
+ 'bid_ad_type': 0
+ }
+ }
+ }
+ }
+ ],
+ 'seat': 'kulturemedia'
+ }
+ ],
+ 'ext': {
+ 'usersync': {
+ 'sovrn': {
+ 'status': 'none',
+ 'syncs': [
+ {
+ 'url': 'urlsovrn',
+ 'type': 'iframe'
+ }
+ ]
+ },
+ 'appnexus': {
+ 'status': 'none',
+ 'syncs': [
+ {
+ 'url': 'urlappnexus',
+ 'type': 'pixel'
+ }
+ ]
+ }
+ },
+ 'responsetimemillis': {
+ 'appnexus': 127
+ }
+ }
+ }
+};
+
+const DEFAULT_NETWORK_ID = 1;
+
+describe('kulturemediaBidAdapter:', function () {
+ let videoBidRequest;
+
+ const VIDEO_REQUEST = {
+ 'bidderCode': 'kulturemedia',
+ 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4',
+ 'bidderRequestId': '34feaad34lkj2',
+ 'bids': videoBidRequest,
+ 'auctionStart': 1520001292880,
+ 'timeout': 3000,
+ 'start': 1520001292884,
+ 'doneCbCallCount': 0,
+ 'refererInfo': {
+ 'numIframes': 1,
+ 'reachedTop': true,
+ 'referer': 'test.com'
+ }
+ };
+
+ beforeEach(function () {
+ videoBidRequest = {
+ mediaTypes: {
+ video: {
+ context: 'instream',
+ playerSize: [[640, 480]],
+ }
+ },
+ bidder: 'kulturemedia',
+ sizes: [640, 480],
+ bidId: '30b3efwfwe1e',
+ adUnitCode: 'video1',
+ params: {
+ video: {
+ playerWidth: 640,
+ playerHeight: 480,
+ mimes: ['video/mp4', 'application/javascript'],
+ protocols: [2, 5],
+ api: [2],
+ position: 1,
+ delivery: [2],
+ sid: 134,
+ rewarded: 1,
+ placement: 1,
+ hp: 1,
+ inventoryid: 123
+ },
+ site: {
+ id: 1,
+ page: 'https://test.com',
+ referrer: 'http://test.com'
+ },
+ publisherId: 'km123'
+ }
+ };
+ });
+
+ describe('isBidRequestValid', function () {
+ context('basic validation', function () {
+ beforeEach(function () {
+ // Basic Valid BidRequest
+ this.bid = {
+ bidder: 'kulturemedia',
+ mediaTypes: {
+ banner: {
+ sizes: [[250, 300]]
+ }
+ },
+ params: {
+ placementId: 'placementId',
+ publisherId: 'publisherId',
+ }
+ };
+ });
+
+ it('should accept request if placementId and publisherId are passed', function () {
+ expect(spec.isBidRequestValid(this.bid)).to.be.true;
+ });
+
+ it('reject requests without params', function () {
+ this.bid.params = {};
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ });
+
+ it('returns false when banner mediaType does not exist', function () {
+ this.bid.mediaTypes = {}
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ });
+ });
+
+ context('banner validation', function () {
+ it('returns true when banner sizes are defined', function () {
+ const bid = {
+ bidder: 'kulturemedia',
+ mediaTypes: {
+ banner: {
+ sizes: [[250, 300]]
+ }
+ },
+ params: {
+ placementId: 'placementId',
+ publisherId: 'publisherId',
+ }
+ };
+
+ expect(spec.isBidRequestValid(bid)).to.be.true;
+ });
+
+ it('returns false when banner sizes are invalid', function () {
+ const invalidSizes = [
+ undefined,
+ '2:1',
+ 123,
+ 'test'
+ ];
+
+ invalidSizes.forEach((sizes) => {
+ const bid = {
+ bidder: 'kulturemedia',
+ mediaTypes: {
+ banner: {
+ sizes
+ }
+ },
+ params: {
+ placementId: 'placementId',
+ publisherId: 'publisherId',
+ }
+ };
+
+ expect(spec.isBidRequestValid(bid)).to.be.false;
+ });
+ });
+ });
+
+ context('video validation', function () {
+ beforeEach(function () {
+ // Basic Valid BidRequest
+ this.bid = {
+ bidder: 'kulturemedia',
+ mediaTypes: {
+ video: {
+ playerSize: [[300, 50]],
+ context: 'instream',
+ mimes: ['foo', 'bar'],
+ protocols: [1, 2]
+ }
+ },
+ params: {
+ placementId: 'placementId',
+ publisherId: 'publisherId',
+ }
+ };
+ });
+
+ it('should return true (skip validations) when e2etest = true', function () {
+ this.bid.params = {
+ e2etest: true
+ };
+ expect(spec.isBidRequestValid(this.bid)).to.equal(true);
+ });
+
+ it('returns false when video context is not defined', function () {
+ delete this.bid.mediaTypes.video.context;
+
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ });
+
+ it('returns false when video playserSize is invalid', function () {
+ const invalidSizes = [
+ undefined,
+ '2:1',
+ 123,
+ 'test'
+ ];
+
+ invalidSizes.forEach((playerSize) => {
+ this.bid.mediaTypes.video.playerSize = playerSize;
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ });
+ });
+
+ it('returns false when video mimes is invalid', function () {
+ const invalidMimes = [
+ undefined,
+ 'test',
+ 1,
+ []
+ ]
+
+ invalidMimes.forEach((mimes) => {
+ this.bid.mediaTypes.video.mimes = mimes;
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ })
+ });
+
+ it('returns false when video protocols is invalid', function () {
+ const invalidMimes = [
+ undefined,
+ 'test',
+ 1,
+ []
+ ]
+
+ invalidMimes.forEach((protocols) => {
+ this.bid.mediaTypes.video.protocols = protocols;
+ expect(spec.isBidRequestValid(this.bid)).to.be.false;
+ })
+ });
+ });
+ });
+
+ describe('buildRequests', function () {
+ context('when mediaType is banner', function () {
+ it('creates request data', function () {
+ let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST);
+
+ expect(request).to.exist.and.to.be.a('object');
+ const payload = JSON.parse(request.data);
+ expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId);
+ expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId);
+ });
+
+ it('has gdpr data if applicable', function () {
+ const req = Object.assign({}, BANNER_REQUEST, {
+ gdprConsent: {
+ consentString: 'consentString',
+ gdprApplies: true,
+ }
+ });
+ let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req);
+
+ const payload = JSON.parse(request.data);
+ expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString);
+ expect(payload.regs.ext).to.have.property('gdpr', 1);
+ });
+
+ it('should properly forward eids parameters', function () {
+ const req = Object.assign({}, BANNER_REQUEST);
+ req.bidRequest[0].userIdAsEids = [
+ {
+ source: 'dummy.com',
+ uids: [
+ {
+ id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a',
+ atype: 1
+ }
+ ]
+ }];
+ let request = spec.buildRequests(req.bidRequest, req);
+
+ const payload = JSON.parse(request.data);
+ expect(payload.user.ext.eids[0].source).to.equal('dummy.com');
+ expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a');
+ expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1);
+ });
+ });
+
+ context('when mediaType is video', function () {
+ it('should create a POST request for every bid', function () {
+ const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST);
+ expect(requests.method).to.equal('POST');
+ expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID);
+ });
+
+ it('should attach request data', function () {
+ const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST);
+ const data = JSON.parse(requests.data);
+ const [width, height] = videoBidRequest.sizes;
+ const VERSION = '1.0.0';
+ expect(data.imp[0].video.w).to.equal(width);
+ expect(data.imp[0].video.h).to.equal(height);
+ expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor);
+ expect(data.ext.prebidver).to.equal('$prebid.version$');
+ expect(data.ext.adapterver).to.equal(spec.VERSION);
+ });
+
+ it('should set pubId to e2etest when bid.params.e2etest = true', function () {
+ videoBidRequest.params.e2etest = true;
+ const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST);
+ expect(requests.method).to.equal('POST');
+ expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID);
+ });
+
+ it('should attach End 2 End test data', function () {
+ videoBidRequest.params.e2etest = true;
+ const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST);
+ const data = JSON.parse(requests.data);
+ expect(data.imp[0].bidfloor).to.not.exist;
+ expect(data.imp[0].video.w).to.equal(640);
+ expect(data.imp[0].video.h).to.equal(480);
+ });
+ });
+ });
+
+ describe('interpretResponse', function () {
+ context('when mediaType is banner', function () {
+ it('have bids', function () {
+ let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST);
+ expect(bids).to.be.an('array').that.is.not.empty;
+ validateBidOnIndex(0);
+ validateBidOnIndex(1);
+
+ function validateBidOnIndex(index) {
+ expect(bids[index]).to.have.property('currency', 'USD');
+ expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid);
+ expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price);
+ expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w);
+ expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h);
+ expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm);
+ expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid);
+ expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain);
+ expect(bids[index]).to.have.property('ttl', 300);
+ expect(bids[index]).to.have.property('netRevenue', true);
+ }
+ });
+
+ it('handles empty response', function () {
+ const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}});
+ const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST);
+
+ expect(bids).to.be.empty;
+ });
+ });
+
+ context('when mediaType is video', function () {
+ it('should return no bids if the response is not valid', function () {
+ const bidResponse = spec.interpretResponse({
+ body: null
+ }, {
+ videoBidRequest
+ });
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return no bids if the response "nurl" and "adm" are missing', function () {
+ const serverResponse = {
+ seatbid: [{
+ bid: [{
+ price: 6.01
+ }]
+ }]
+ };
+ const bidResponse = spec.interpretResponse({
+ body: serverResponse
+ }, {
+ videoBidRequest
+ });
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return no bids if the response "price" is missing', function () {
+ const serverResponse = {
+ seatbid: [{
+ bid: [{
+ adm: ''
+ }]
+ }]
+ };
+ const bidResponse = spec.interpretResponse({
+ body: serverResponse
+ }, {
+ videoBidRequest
+ });
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return a valid video bid response with just "adm"', function () {
+ const serverResponse = {
+ id: '123',
+ seatbid: [{
+ bid: [{
+ id: 1,
+ adid: 123,
+ impid: 456,
+ crid: 2,
+ price: 6.01,
+ adm: '',
+ adomain: [
+ 'kulturemedia.com'
+ ],
+ w: 640,
+ h: 480,
+ ext: {
+ prebid: {
+ type: 'video'
+ },
+ }
+ }]
+ }],
+ cur: 'USD'
+ };
+ const bidResponse = spec.interpretResponse({
+ body: serverResponse
+ }, {
+ videoBidRequest
+ });
+ let o = {
+ requestId: serverResponse.seatbid[0].bid[0].impid,
+ ad: '',
+ bidderCode: spec.code,
+ cpm: serverResponse.seatbid[0].bid[0].price,
+ creativeId: serverResponse.seatbid[0].bid[0].crid,
+ vastXml: serverResponse.seatbid[0].bid[0].adm,
+ width: 640,
+ height: 480,
+ mediaType: 'video',
+ currency: 'USD',
+ ttl: 300,
+ netRevenue: true,
+ meta: {
+ advertiserDomains: ['kulturemedia.com']
+ }
+ };
+ expect(bidResponse[0]).to.deep.equal(o);
+ });
+
+ it('should default ttl to 300', function () {
+ const serverResponse = {
+ seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}],
+ cur: 'USD'
+ };
+ const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest});
+ expect(bidResponse[0].ttl).to.equal(300);
+ });
+ it('should not allow ttl above 3601, default to 300', function () {
+ videoBidRequest.params.video.ttl = 3601;
+ const serverResponse = {
+ seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}],
+ cur: 'USD'
+ };
+ const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest});
+ expect(bidResponse[0].ttl).to.equal(300);
+ });
+ it('should not allow ttl below 1, default to 300', function () {
+ videoBidRequest.params.video.ttl = 0;
+ const serverResponse = {
+ seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}],
+ cur: 'USD'
+ };
+ const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest});
+ expect(bidResponse[0].ttl).to.equal(300);
+ });
+ });
+ });
+
+ describe('getUserSyncs', function () {
+ it('handles no parameters', function () {
+ let opts = spec.getUserSyncs({});
+ expect(opts).to.be.an('array').that.is.empty;
+ });
+ it('returns non if sync is not allowed', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false});
+
+ expect(opts).to.be.an('array').that.is.empty;
+ });
+
+ it('iframe sync enabled should return results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]);
+
+ expect(opts.length).to.equal(1);
+ expect(opts[0].type).to.equal('iframe');
+ expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url);
+ });
+
+ it('pixel sync enabled should return results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]);
+
+ expect(opts.length).to.equal(1);
+ expect(opts[0].type).to.equal('image');
+ expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url);
+ });
+
+ it('all sync enabled should return all results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]);
+
+ expect(opts.length).to.equal(2);
+ });
+ });
+})
+;