diff --git a/modules/dailymotionBidAdapter.js b/modules/dailymotionBidAdapter.js index 791fbccda5f..8b34e674fde 100644 --- a/modules/dailymotionBidAdapter.js +++ b/modules/dailymotionBidAdapter.js @@ -1,4 +1,5 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { VIDEO } from '../src/mediaTypes.js'; import { deepAccess } from '../src/utils.js'; import { config } from '../src/config.js'; @@ -6,6 +7,37 @@ import { userSync } from '../src/userSync.js'; const DAILYMOTION_VENDOR_ID = 573; +const dailymotionOrtbConverter = ortbConverter({ + context: { + netRevenue: true, + ttl: 600, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (typeof bidRequest.getFloor === 'function') { + const size = imp.w > 0 && imp.h > 0 ? [imp.w, imp.h] : '*'; + + const floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', // or '*' for all the mediaType + size + }) || {}; + + if (floorInfo.floor && floorInfo.currency) { + imp.bidfloor = floorInfo.floor; + imp.bidfloorcur = floorInfo.currency; + } + } + + return imp; + }, +}); + +function isArrayFilled (_array) { + return _array && Array.isArray(_array) && _array.length > 0; +} + /** * Get video metadata from bid request * @@ -23,6 +55,10 @@ function getVideoMetadata(bidRequest, bidderRequest) { // Content object is either from Object: Site or Object: App const contentObj = deepAccess(siteOrAppObj, 'content') + const contentCattax = deepAccess(contentObj, 'cattax', 0); + const isContentCattaxV1 = contentCattax === 1; + const isContentCattaxV2 = [2, 5, 6].includes(contentCattax); + const parsedContentData = { // Store as object keys to ensure uniqueness iabcat1: {}, @@ -49,14 +85,16 @@ function getVideoMetadata(bidRequest, bidderRequest) { const videoMetadata = { description: videoParams.description || '', duration: videoParams.duration || deepAccess(contentObj, 'len', 0), - iabcat1: Array.isArray(videoParams.iabcat1) + iabcat1: isArrayFilled(videoParams.iabcat1) ? videoParams.iabcat1 - : Array.isArray(deepAccess(contentObj, 'cat')) + : (isArrayFilled(deepAccess(contentObj, 'cat')) && isContentCattaxV1) ? contentObj.cat : Object.keys(parsedContentData.iabcat1), - iabcat2: Array.isArray(videoParams.iabcat2) + iabcat2: isArrayFilled(videoParams.iabcat2) ? videoParams.iabcat2 - : Object.keys(parsedContentData.iabcat2), + : (isArrayFilled(deepAccess(contentObj, 'cat')) && isContentCattaxV2) + ? contentObj.cat + : Object.keys(parsedContentData.iabcat2), id: videoParams.id || deepAccess(contentObj, 'id', ''), lang: videoParams.lang || deepAccess(contentObj, 'language', ''), livestream: typeof videoParams.livestream === 'number' @@ -153,6 +191,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests = [], bidderRequest) { + const ortbData = dailymotionOrtbConverter.toORTB({ bidRequests: validBidRequests, bidderRequest }); // check consent to be able to read user cookie const allowCookieReading = // No GDPR applies @@ -184,6 +223,7 @@ export const spec = { url: 'https://pb.dmxleo.com', data: { pbv: '$prebid.version$', + ortb: ortbData, bidder_request: { gdprConsent: { apiVersion: deepAccess(bidderRequest, 'gdprConsent.apiVersion', 1), @@ -206,20 +246,6 @@ export const spec = { api_key: bid.params.apiKey, ts: bid.params.dmTs, }, - // Cast boolean in any case (value should be 0 or 1) to ensure type - coppa: !!deepAccess(bidderRequest, 'ortb2.regs.coppa'), - // In app context, we need to retrieve additional informations - ...(!deepAccess(bidderRequest, 'ortb2.site') && !!deepAccess(bidderRequest, 'ortb2.app') ? { - appBundle: deepAccess(bidderRequest, 'ortb2.app.bundle', ''), - appStoreUrl: deepAccess(bidderRequest, 'ortb2.app.storeurl', ''), - } : {}), - ...(deepAccess(bidderRequest, 'ortb2.device') ? { - device: { - lmt: deepAccess(bidderRequest, 'ortb2.device.lmt', null), - ifa: deepAccess(bidderRequest, 'ortb2.device.ifa', ''), - atts: deepAccess(bidderRequest, 'ortb2.device.ext.atts', 0), - }, - } : {}), userSyncEnabled: isUserSyncEnabled(), request: { adUnitCode: deepAccess(bid, 'adUnitCode', ''), @@ -261,7 +287,7 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: serverResponse => serverResponse?.body ? [serverResponse.body] : [], + interpretResponse: serverResponse => serverResponse?.body?.cpm ? [serverResponse.body] : [], /** * Retrieves user synchronization URLs based on provided options and consents. diff --git a/modules/dailymotionBidAdapter.md b/modules/dailymotionBidAdapter.md index 21cc6c49205..12d5bc1c04d 100644 --- a/modules/dailymotionBidAdapter.md +++ b/modules/dailymotionBidAdapter.md @@ -11,6 +11,16 @@ Maintainer: ad-leo-engineering@dailymotion.com Dailymotion prebid adapter. Supports video ad units in instream context. +### Usage + +Make sure to have the following modules listed while building prebid : `priceFloors,dailymotionBidAdapter` + +`priceFloors` module is needed to retrieve the price floor: https://docs.prebid.org/dev-docs/modules/floors.html + +```shell +gulp build --modules=priceFloors,dailymotionBidAdapter +``` + ### Configuration options Before calling this adapter, you need to at least set a video adUnit in an instream context and the API key in the bid parameters: @@ -58,6 +68,116 @@ pbjs.setConfig({ }); ``` +#### Price floor + +The price floor can be set at the ad unit level, for example : + +```javascript +const adUnits = [{ + floors: { + currency: 'USD', + schema: { + fields: [ 'mediaType', 'size' ] + }, + values: { + 'video|300x250': 2.22, + 'video|*': 1 + } + }, + bids: [{ + bidder: 'dailymotion', + params: { + apiKey: 'dailymotion-testing', + } + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'instream', + }, + } +}]; + +// Do not forget to set an empty object for "floors" to active the price floor module +pbjs.setConfig({floors: {}}); +``` + +The following request will be sent to Dailymotion Prebid Service : + +```javascript +{ + "pbv": "9.23.0-pre", + "ortb": { + "imp": [ + { + ... + "bidfloor": 2.22, + "bidfloorcur": "USD" + } + ], + } + ... +} +``` + +Or the price floor can be set at the package level, for example : + +```javascript +const adUnits = [ + { + bids: [{ + bidder: 'dailymotion', + params: { + apiKey: 'dailymotion-testing', + } + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + playerSize: [1280,720], + context: 'instream', + }, + } + } +]; + +pbjs.setConfig({ + floors: { + data: { + currency: 'USD', + schema: { + fields: [ 'mediaType', 'size' ] + }, + values: { + 'video|300x250': 2.22, + 'video|*': 1 + } + } + } +}) +``` + +This will send the following bid floor in the request to Daiymotion Prebid Service : + +```javascript +{ + "pbv": "9.23.0-pre", + "ortb": { + "imp": [ + { + ... + "bidfloor": 1, + "bidfloorcur": "USD" + } + ], + ... + } +} +``` + +You can also [set dynamic floors](https://docs.prebid.org/dev-docs/modules/floors.html#bid-adapter-interface). + ### Test Parameters By setting the following bid parameters, you'll get a constant response to any request, to validate your adapter integration: @@ -142,6 +262,7 @@ const adUnits = [ private: false, tags: 'tag_1,tag_2,tag_3', title: 'test video', + url: 'https://test.com/testvideo' topics: 'topic_1, topic_2', isCreatedForKids: false, videoViewsInSession: 1, @@ -161,7 +282,7 @@ const adUnits = [ maxduration: 30, playbackmethod: [3], plcmt: 1, - protocols: [7, 8, 11, 12, 13, 14] + protocols: [7, 8, 11, 12, 13, 14], startdelay: 0, w: 1280, h: 720, @@ -206,16 +327,4 @@ If you already specify [First-Party data](https://docs.prebid.org/features/first | `ortb2.site.content.keywords` | `tags` | | `ortb2.site.content.title` | `title` | | `ortb2.site.content.url` | `url` | -| `ortb2.app.bundle` | N/A | -| `ortb2.app.storeurl` | N/A | -| `ortb2.device.lmt` | N/A | -| `ortb2.device.ifa` | N/A | -| `ortb2.device.ext.atts` | N/A | - -### Integrating the adapter - -To use the adapter with any non-test request, you first need to ask an API key from Dailymotion. Please contact us through **DailymotionPrebid.js@dailymotion.com**. - -You will then be able to use it within the bid parameters before making a bid request. - -This API key will ensure proper identification of your inventory and allow you to get real bids. +| `ortb2.*` | N/A | diff --git a/test/spec/modules/dailymotionBidAdapter_spec.js b/test/spec/modules/dailymotionBidAdapter_spec.js index 2a276a06b15..3f420c1a48a 100644 --- a/test/spec/modules/dailymotionBidAdapter_spec.js +++ b/test/spec/modules/dailymotionBidAdapter_spec.js @@ -191,7 +191,6 @@ describe('dailymotionBidAdapterTests', () => { }); expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); expect(reqData.config.ts).to.eql(bidRequestData[0].params.dmTs); - expect(reqData.coppa).to.be.true; expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); expect(reqData.request.mediaTypes.video).to.eql(bidRequestData[0].mediaTypes.video); @@ -1403,6 +1402,7 @@ describe('dailymotionBidAdapterTests', () => { it('validates buildRequests with content values from App', () => { const bidRequestData = [{ + getFloor: () => ({ currency: 'USD', floor: 3 }), auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId: 123456, adUnitCode: 'preroll', @@ -1448,6 +1448,7 @@ describe('dailymotionBidAdapterTests', () => { }]; const bidderRequestData = { + timeout: 4242, refererInfo: { page: 'https://publisher.com', }, @@ -1462,14 +1463,30 @@ describe('dailymotionBidAdapterTests', () => { applicableSections: [5], }, ortb2: { + bcat: ['IAB-1'], + badv: ['bcav-1'], regs: { coppa: 1, }, device: { lmt: 1, ifa: 'xxx', + devicetype: 2, + make: 'make', + model: 'model', + os: 'os', + osv: 'osv', + language: 'language', + geo: { + country: 'country', + region: 'region', + city: 'city', + zip: 'zip', + metro: 'metro' + }, ext: { atts: 2, + ifa_type: 'ifa_type' }, }, app: { @@ -1477,6 +1494,7 @@ describe('dailymotionBidAdapterTests', () => { storeurl: 'https://play.google.com/store/apps/details?id=app-bundle', content: { len: 556, + cattax: 3, data: [ { name: 'dataprovider.com', @@ -1516,6 +1534,117 @@ describe('dailymotionBidAdapterTests', () => { expect(request.url).to.equal('https://pb.dmxleo.com'); expect(reqData.pbv).to.eql('$prebid.version$'); + + const expectedOrtb = { + 'app': { + 'bundle': 'app-bundle', + 'content': { + 'cattax': 3, + 'data': [ + { + 'ext': { + 'segtax': 4 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': 'IAB-1' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '200' + } + ], + } + ], + 'len': 556, + }, + 'storeurl': 'https://play.google.com/store/apps/details?id=app-bundle', + }, + 'badv': [ + 'bcav-1' + ], + 'bcat': [ + 'IAB-1' + ], + 'device': { + 'devicetype': 2, + 'ext': { + 'atts': 2, + 'ifa_type': 'ifa_type' + }, + 'geo': { + 'city': 'city', + 'country': 'country', + 'metro': 'metro', + 'region': 'region', + 'zip': 'zip', + }, + 'ifa': 'xxx', + 'language': 'language', + 'lmt': 1, + 'make': 'make', + 'model': 'model', + 'os': 'os', + 'osv': 'osv', + }, + 'imp': [{ + 'bidfloor': 3, + 'bidfloorcur': 'USD', + 'id': 123456, + 'secure': 1, + ...(FEATURES.VIDEO ? { + 'video': { + 'api': [ + 2, + 7 + ], + 'h': 720, + 'maxduration': 30, + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'playbackmethod': [ + 3 + ], + 'plcmt': 1, + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'skipafter': 5, + 'skipmin': 10, + 'startdelay': 0, + 'w': 1280, + } + } : {}), + } + ], + 'regs': { + 'coppa': 1, + }, + 'test': 0, + 'tmax': 4242, + } + + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql(expectedOrtb); expect(reqData.userSyncEnabled).to.be.true; expect(reqData.bidder_request).to.eql({ refererInfo: bidderRequestData.refererInfo, @@ -1524,12 +1653,6 @@ describe('dailymotionBidAdapterTests', () => { gppConsent: bidderRequestData.gppConsent, }); expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); - expect(reqData.coppa).to.be.true; - expect(reqData.appBundle).to.eql(bidderRequestData.ortb2.app.bundle); - expect(reqData.appStoreUrl).to.eql(bidderRequestData.ortb2.app.storeurl); - expect(reqData.device.lmt).to.eql(bidderRequestData.ortb2.device.lmt); - expect(reqData.device.ifa).to.eql(bidderRequestData.ortb2.device.ifa); - expect(reqData.device.atts).to.eql(bidderRequestData.ortb2.device.ext.atts); expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); @@ -1601,6 +1724,7 @@ describe('dailymotionBidAdapterTests', () => { gdprApplies: true, }, ortb2: { + tmax: 31416, regs: { gpp: 'xxx', gpp_sid: [5], @@ -1616,6 +1740,7 @@ describe('dailymotionBidAdapterTests', () => { url: 'https://test.com/test', livestream: 1, cat: ['IAB-2'], + cattax: 1, data: [ undefined, // Undefined to check proper handling of edge cases {}, // Empty object to check proper handling of edge cases @@ -1686,7 +1811,133 @@ describe('dailymotionBidAdapterTests', () => { expect(request.url).to.equal('https://pb.dmxleo.com'); + const expectedOrtb = { + 'imp': [{ + 'id': 123456, + 'secure': 1, + ...(FEATURES.VIDEO ? { + 'video': { + 'api': [ + 2, + 7 + ], + 'startdelay': 0, + } + } : {}) + } + ], + 'regs': { + 'coppa': 0, + 'gpp': 'xxx', + 'gpp_sid': [ + 5 + ], + }, + 'site': { + 'cat': [ + 'IAB-1', + ], + 'content': { + 'cat': [ + 'IAB-2', + ], + 'cattax': 1, + 'data': [ + undefined, + {}, + { + 'ext': {} + }, + { + 'ext': { + 'segtax': 22 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '400' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': undefined + }, + { + 'ext': { + 'segtax': 4 + }, + 'name': 'dataprovider.com', + 'segment': undefined + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': 2222 + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '6' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '6' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '17' + }, + { + 'id': '20' + } + ] + } + ], + 'id': '54321', + 'keywords': 'tag_1,tag_2,tag_3', + 'language': 'FR', + 'livestream': 1, + 'title': 'test video', + 'url': 'https://test.com/test', + } + }, + 'test': 0, + 'tmax': 31416 + } + expect(reqData.pbv).to.eql('$prebid.version$'); + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql(expectedOrtb); + expect(reqData.userSyncEnabled).to.be.true; expect(reqData.bidder_request).to.eql({ refererInfo: bidderRequestData.refererInfo, @@ -1698,7 +1949,6 @@ describe('dailymotionBidAdapterTests', () => { }, }); expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); - expect(reqData.coppa).to.be.false; expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); @@ -1763,11 +2013,20 @@ describe('dailymotionBidAdapterTests', () => { const { data: reqData } = request; expect(request.url).to.equal('https://pb.dmxleo.com'); - expect(reqData.config.api_key).to.eql(bidRequestDataWithApi[0].params.apiKey); - expect(reqData.coppa).to.be.false; - expect(reqData.pbv).to.eql('$prebid.version$'); + + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql({ + 'imp': [ + { + 'id': undefined, + 'secure': 1 + }, + ], + 'test': 0 + }); expect(reqData.userSyncEnabled).to.be.false; expect(reqData.bidder_request).to.eql({ gdprConsent: { @@ -1834,6 +2093,187 @@ describe('dailymotionBidAdapterTests', () => { }); }); + describe('validates buildRequests for video metadata iabcat1 and iabcat2', () => { + let bidRequestData; + let bidderRequestData; + let request; + + beforeEach(() => { + bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + startdelay: 0, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + iabcat1: ['video-params-iabcat1'], + iabcat2: ['video-params-iabcat2'], + }, + }, + }]; + + bidderRequestData = { + timeout: 4242, + refererInfo: { + page: 'https://publisher.com', + }, + ortb2: { + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [{ id: '1' }], + }, + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '6' }], + }, + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '17' }, { id: '20' }], + }, + ] + }, + } + } + }; + + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: ['dailymotion'], + filter: 'include' + }, + iframe: { + bidders: ['dailymotion'], + filter: 'exclude', + }, + }, + }, + }); + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + }); + + it('get iabcat1 and iabcat 2 from params video', () => { + expect(request.data.video_metadata.iabcat1).to.eql(bidRequestData[0].params.video.iabcat1); + expect(request.data.video_metadata.iabcat2).to.eql(bidRequestData[0].params.video.iabcat2); + }) + + it('get iabcat1 from content.cat and iabcat2 from data.segment', () => { + const iabCatTestsCases = [[], null, {}]; + + iabCatTestsCases.forEach((iabCat) => { + bidRequestData[0].params.video.iabcat1 = iabCat; + bidRequestData[0].params.video.iabcat2 = iabCat; + bidderRequestData.ortb2.site.content.cat = ['video-content-cat']; + bidderRequestData.ortb2.site.content.cattax = 1; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(bidderRequestData.ortb2.site.content.cat); + expect(request.data.video_metadata.iabcat2).to.eql(['6', '17', '20']); + }) + }) + + it('get iabcat2 from content.cat and iabcat1 from data.segment', () => { + const iabCatTestsCases = [[], null, {}]; + const cattaxV2 = [2, 5, 6]; + + cattaxV2.forEach((cattax) => { + iabCatTestsCases.forEach((iabCat) => { + bidRequestData[0].params.video.iabcat1 = iabCat; + bidRequestData[0].params.video.iabcat2 = iabCat; + bidderRequestData.ortb2.site.content.cat = ['video-content-cat']; + bidderRequestData.ortb2.site.content.cattax = cattax; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(['1']); + expect(request.data.video_metadata.iabcat2).to.eql(bidderRequestData.ortb2.site.content.cat); + }) + }) + }) + + it('get iabcat1 and iabcat2 from data.segmnet', () => { + const contentCatTestCases = [[], null, {}]; + const cattaxTestCases = [1, 2, 5, 6]; + + cattaxTestCases.forEach((cattax) => { + contentCatTestCases.forEach((contentCat) => { + bidRequestData[0].params.video.iabcat1 = []; + bidRequestData[0].params.video.iabcat2 = []; + bidderRequestData.ortb2.site.content.cat = contentCat; + bidderRequestData.ortb2.site.content.cattax = cattax; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(['1']); + expect(request.data.video_metadata.iabcat2).to.eql(['6', '17', '20']); + }) + }) + }) + }); + + it('validates buildRequests - with null floor as object for getFloor function', () => { + const bidRequest = [{ + params: { + apiKey: 'test_api_key', + }, + getFloor: () => null + }]; + + config.setConfig({ + userSync: { + syncEnabled: false, + } + }); + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequest, {}), + ); + + const { data: reqData } = request; + + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql({ + 'imp': [ + { + 'id': undefined, + 'secure': 1 + }, + ], + 'test': 0 + }); + }) + it('validates buildRequests - with empty/undefined validBidRequests', () => { expect(spec.buildRequests([], {})).to.have.lengthOf(0); @@ -1862,6 +2302,17 @@ describe('dailymotionBidAdapterTests', () => { expect(bid).to.eql(serverResponse.body); }); + it('validates interpretResponse - without bid (no cpm)', () => { + const serverResponse = { + body: { + requestId: 'test_requestid', + }, + }; + + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(0); + }); + it('validates interpretResponse - with empty/undefined serverResponse', () => { expect(spec.interpretResponse({})).to.have.lengthOf(0);