From b4296cd2e8db73350c207a654f3bec955dd608d8 Mon Sep 17 00:00:00 2001 From: kmdevops <126434358+kmdevops@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:40:42 +0200 Subject: [PATCH] DXKulture Bid Adapter: implementing ORTB conversion library (#10584) --- modules/dxkultureBidAdapter.js | 407 ++------ modules/dxkultureBidAdapter.md | 10 +- test/spec/modules/dxkultureBidAdapter_spec.js | 936 ++++++++++-------- 3 files changed, 614 insertions(+), 739 deletions(-) diff --git a/modules/dxkultureBidAdapter.js b/modules/dxkultureBidAdapter.js index a8f7b4b86ba..9e4768d12bb 100644 --- a/modules/dxkultureBidAdapter.js +++ b/modules/dxkultureBidAdapter.js @@ -1,39 +1,86 @@ import { - deepSetValue, logInfo, - deepAccess, logError, - isFn, - isPlainObject, - isStr, - isNumber, - isArray, logMessage + logMessage, + deepAccess, + deepSetValue, + mergeDeep } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' const BIDDER_CODE = 'dxkulture'; 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', - 'plcmt', - 'protocols', - 'startdelay', - 'skip', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackmethod', - 'api', - 'linearity' -]; +const DEFAULT_CURRENCY = 'USD'; +const SYNC_URL = 'https://ads.kulture.media/usync'; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + } + }) + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return req; + }, + bidResponse(buildBidResponse, bid, context) { + let resMediaType; + if (bid.adm?.trim().startsWith(' 0) { - deepSetValue(openrtbRequest, 'user.ext.eids', eids); - } + const data = converter.toORTB({ bidRequests: validBidRequests, bidderRequest, context: {contextMediaType} }); 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'); + logMessage('dxkulture: E2E test mode enabled'); publisherId = 'e2etest' } let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; @@ -111,70 +123,46 @@ export const spec = { if (placementId) { baseEndpoint += '&placementId=' + placementId } - if (networkId) { - baseEndpoint += '&nId=' + networkId - } - const payloadString = JSON.stringify(openrtbRequest); return { method: 'POST', url: baseEndpoint, - data: payloadString, + data: data }; }, - 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('dxkulture.interpretResponse :: no valid responses to interpret'); - } - return bidResponses; + interpretResponse: function (serverResponse, bidRequest) { + const bids = converter.fromORTB({response: serverResponse.body, request: bidRequest.data}).bids; + return bids; }, - getUserSyncs: function (syncOptions, serverResponses) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { logInfo('dxkulture.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') - } + if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) { + let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let queryParamStrings = []; + let syncUrl = SYNC_URL; + if (gdprConsent) { + queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); } - }); - logInfo('dxkulture.getUserSyncs result=%o', syncs); - return syncs; - }, + + return [{ + type: pixelType, + url: `${syncUrl}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}` + }]; + } + } }; @@ -182,16 +170,10 @@ export const spec = { * 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'); } @@ -206,12 +188,12 @@ function _validateParams(bidRequest) { } if (!bidRequest.params.publisherId) { - logError('Validation failed: publisherId not declared'); + logError('dxkulture: Validation failed: publisherId not declared'); return false; } if (!bidRequest.params.placementId) { - logError('Validation failed: placementId not declared'); + logError('dxkulture: Validation failed: placementId not declared'); return false; } @@ -266,204 +248,31 @@ function _validateVideo(bidRequest) { }; if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - logError('Validation failed: mimes are invalid'); + logError('dxkulture: Validation failed: mimes are invalid'); return false; } if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - logError('Validation failed: protocols are invalid'); + logError('dxkulture: Validation failed: protocols are invalid'); return false; } if (!videoParams.context) { - logError('Validation failed: context id not declared'); + logError('dxkulture: Validation failed: context id not declared'); return false; } if (videoParams.context !== 'instream') { - logError('Validation failed: only context instream is supported '); + logError('dxkulture: 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]]'); + logError('dxkulture: 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]; - } - }); - - // - 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('DXKulture 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/dxkultureBidAdapter.md b/modules/dxkultureBidAdapter.md index e934aee3301..e31794ef6c6 100644 --- a/modules/dxkultureBidAdapter.md +++ b/modules/dxkultureBidAdapter.md @@ -30,7 +30,8 @@ var adUnits = [ params: { placementId: 'test', publisherId: 'test', - networkId: '123' + bidfloor: 2.7, + bidfloorcur: 'USD' } }] } @@ -43,7 +44,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid - 'mimes', - 'minduration', - 'maxduration', -- 'placement', +- 'plcmt', - 'protocols', - 'startdelay', - 'skip', @@ -74,7 +75,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid delivery: [2], minduration: 10, maxduration: 30, - placement: 1, + plcmt: 1, playbackmethod: [1,5], } }, @@ -84,8 +85,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid params: { bidfloor: 0.5, publisherId: '12345', - placementId: '6789', - networkId" '123' + placementId: '6789' } } ] diff --git a/test/spec/modules/dxkultureBidAdapter_spec.js b/test/spec/modules/dxkultureBidAdapter_spec.js index ec7f6f146a3..d3ae8ec5124 100644 --- a/test/spec/modules/dxkultureBidAdapter_spec.js +++ b/test/spec/modules/dxkultureBidAdapter_spec.js @@ -1,137 +1,198 @@ import {expect} from 'chai'; -import {spec} from 'modules/dxkultureBidAdapter.js'; - -const BANNER_REQUEST = { - 'bidderCode': 'dxkulture', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', - 'bidderRequestId': 'requestId', - 'bidRequest': [{ - 'bidder': 'dxkulture', - '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': 'dxkulture', - '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 +import {spec, SYNC_URL} from 'modules/dxkultureBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'dxkulture', + params: { + placementId: 123456, + publisherId: 'publisherId', + bidfloor: 10, + }, + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + placementCode: 'div-gpt-dummy-placement-code', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: '2e9f38ea93bb9e', + bidderRequestId: 'bidderRequestId', + } + ], + 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 +const getVideoRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + bidderRequestId: '34feaad34lkj2', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + 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, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe2e', + 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, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }], + auctionStart: 1520001292880, + timeout: 5000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: '2e9f38ea93bb9e', + impid: '2e9f38ea93bb9e', + 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 + } } } } + ], + seat: 'dxkulture' + } + ], + ext: { + usersync: { + sovrn: { + status: 'none', + syncs: [ + { + url: 'urlsovrn', + type: 'iframe' + } + ] }, - { - '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 - } + appnexus: { + status: 'none', + syncs: [ + { + url: 'urlappnexus', + type: 'pixel' } - } + ] } - ], - 'seat': 'dxkulture' - } - ], - 'ext': { - 'usersync': { - 'sovrn': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlsovrn', - 'type': 'iframe' - } - ] }, - 'appnexus': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlappnexus', - 'type': 'pixel' - } - ] + responsetimemillis: { + appnexus: 127 } - }, - 'responsetimemillis': { - 'appnexus': 127 } } - } -}; - -const DEFAULT_NETWORK_ID = 1; + }; +} -describe('dxkultureBidAdapter:', function () { +describe('dxkultureBidAdapter', function() { let videoBidRequest; const VIDEO_REQUEST = { @@ -183,51 +244,86 @@ describe('dxkultureBidAdapter:', function () { page: 'https://test.com', referrer: 'http://test.com' }, - publisherId: 'km123' + publisherId: 'km123', + bidfloor: 0 } }; }); - describe('isBidRequestValid', function () { - context('basic validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'dxkulture', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); + describe('isValidRequest', function() { + let bidderRequest; - it('should accept request if placementId and publisherId are passed', function () { - expect(spec.isBidRequestValid(this.bid)).to.be.true; - }); + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); - it('reject requests without params', function () { - this.bid.params = {}; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); - it('returns false when banner mediaType does not exist', function () { - this.bid.mediaTypes = {} - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.kulture.media/pbjs?pid=publisherId&placementId=123456'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; }); - context('banner validation', function () { - it('returns true when banner sizes are defined', function () { + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { const bid = { bidder: 'dxkulture', mediaTypes: { banner: { - sizes: [[250, 300]] + sizes } }, params: { @@ -236,350 +332,281 @@ describe('dxkultureBidAdapter:', function () { } }; - expect(spec.isBidRequestValid(bid)).to.be.true; + expect(spec.isBidRequestValid(bid)).to.be.false; }); + }); + }); - it('returns false when banner sizes are invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((sizes) => { - const bid = { - bidder: 'dxkulture', - mediaTypes: { - banner: { - sizes - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'dxkulture', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); }); - context('video validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'dxkulture', - mediaTypes: { - video: { - playerSize: [[300, 50]], - context: 'instream', - mimes: ['foo', 'bar'], - protocols: [1, 2] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; - it('should return true (skip validations) when e2etest = true', function () { - this.bid.params = { - e2etest: true - }; - expect(spec.isBidRequestValid(this.bid)).to.equal(true); - }); + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); - it('returns false when video context is not defined', function () { - delete this.bid.mediaTypes.video.context; + 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 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, + [] + ] - 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; - }) - }); + 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; - }) - }); + 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 () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 + } + } + }, { + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); + context('when mediaType is banner', function () { it('creates request data', function () { - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) 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); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); }); it('has gdpr data if applicable', function () { - const req = Object.assign({}, BANNER_REQUEST, { + const req = Object.assign({}, getBannerRequest(), { gdprConsent: { consentString: 'consentString', gdprApplies: true, } }); - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); + let request = spec.buildRequests(bidderBannerRequest.bids, req); - const payload = JSON.parse(request.data); + const payload = 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); + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId); + }); - 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); - }); - }); + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); - 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 set pubId to e2etest when bid.params.e2etest = true', function () { + bidRequestsWithMediaTypes[0].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest'); + }); - 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.imp[0]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); - expect(data.imp[0]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); - expect(data.ext.prebidver).to.equal('$prebid.version$'); - expect(data.ext.adapterver).to.equal(spec.VERSION); + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); }); + } + }); - 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); + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); }); - 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); + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; }); - }); - }); - describe('interpretResponse', function () { - context('when mediaType is banner', function () { it('have bids', function () { - let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); + let bids = spec.interpretResponse(bidderResponse, bidRequest); 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('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); expect(bids[index]).to.have.property('ttl', 300); expect(bids[index]).to.have.property('netRevenue', true); } }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); it('handles empty response', function () { - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); 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 = { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { seatbid: [{ bid: [{ price: 6.01 }] }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); }); it('should return no bids if the response "price" is missing', function () { - const serverResponse = { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { 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: [ - 'dxkulture.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: ['dxkulture.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); + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); }); }); }); - describe('getUserSyncs', function () { + describe('user sync', function () { it('handles no parameters', function () { let opts = spec.getUserSyncs({}); expect(opts).to.be.an('array').that.is.empty; @@ -590,27 +617,66 @@ describe('dxkultureBidAdapter:', function () { 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]); + describe('when gdpr applies', function () { + let gdprConsent; + let gdprPixelUrl; + const consentString = 'gdpr-pixel-consent'; + const gdprApplies = '1'; + beforeEach(() => { + gdprConsent = { + consentString, + gdprApplies: true + }; - 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); - }); + gdprPixelUrl = `${SYNC_URL}&gdpr=${gdprApplies}&gdpr_consent=${consentString}`; + }); - it('pixel sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + it('when there is a response, it should have the gdpr query params', () => { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + gdprConsent + ); - 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); + expect(url).to.have.string(`gdpr_consent=${consentString}`); + expect(url).to.have.string(`gdpr=${gdprApplies}`); + }); + + it('should not send signals if no consent object is available', function () { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + ); + expect(url).to.not.have.string('gdpr_consent='); + expect(url).to.not.have.string('gdpr='); + }); }); - it('all sync enabled should return all results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + describe('when ccpa applies', function () { + let usPrivacyConsent; + let uspPixelUrl; + const privacyString = 'TEST'; + beforeEach(() => { + usPrivacyConsent = 'TEST'; + uspPixelUrl = `${SYNC_URL}&us_privacy=${privacyString}` + }); + it('should send the us privacy string, ', () => { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + undefined, + usPrivacyConsent + ); + expect(url).to.have.string(`us_privacy=${privacyString}`); + }); - expect(opts.length).to.equal(2); + it('should not send signals if no consent string is available', function () { + let [{url}] = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [], + ); + expect(url).to.not.have.string('us_privacy='); + }); }); }); -}) -; +});