diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 42c7508915..6a54eabd6a 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,32 +1,229 @@ -import { logError } from '../src/utils.js'; +import { deepClone, deepSetValue, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; -const VERSION = 'v1.0.3'; +const VERSION = 'v1.1.0'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; const IAB_GVL_ID = 811; +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NETREVENUE, + ttl: DEFAULT_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { id, dimension } = bidRequest.params || {}; + + deepSetValue(imp, 'ext.bidder.id', id); + if (dimension != null) { + deepSetValue(imp, 'ext.bidder.dimension', dimension); + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const refererInfo = bidderRequest?.refererInfo; + + if (refererInfo) { + const ext = {}; + if (refererInfo.reachedTop != null) ext.reachedTop = refererInfo.reachedTop; + if (refererInfo.numIframes != null) ext.numIframes = refererInfo.numIframes; + if (refererInfo.stack != null) ext.stack = refererInfo.stack; + + if (Object.keys(ext).length) { + if (!request.site) request.site = {}; + request.site.ext = Object.assign({}, request.site.ext, ext); + } + } + + if (!request.ext) request.ext = {}; + request.ext.adapterVersion = VERSION; + + return request; + } +}); + +function logMissingFields(scope, missingFields) { + if (missingFields.length) { + logWarn(`${BIDDER_CODE}: Missing ${scope} fields: ${missingFields.join(', ')}`); + } +} + +function isValidEndpointUrl(endpoint) { + try { + const parsedEndpoint = new URL(endpoint); + return parsedEndpoint.protocol === 'https:'; + } catch (e) { + return false; + } +} + +function resolveEndpoint(endpoint) { + if (typeof endpoint === 'string') { + if (isValidEndpointUrl(endpoint)) { + return endpoint; + } else { + logWarn(`${BIDDER_CODE}: Endpoint ${endpoint} is not a valid HTTPS URL, using default endpoint`); + } + } + + return ENDPOINT_URL; +} + +function extractReferer(refererInfo) { + if (!refererInfo) { + return null; + } + + const refererUrl = refererInfo.topmostLocation ?? refererInfo.ref; + const missingRefererFields = []; + + if (refererInfo.reachedTop == null) missingRefererFields.push('reachedTop'); + if (refererUrl == null) missingRefererFields.push('referer'); + if (refererInfo.numIframes == null) missingRefererFields.push('numIframes'); + if (refererInfo.stack == null) missingRefererFields.push('stack'); + + logMissingFields('referer', missingRefererFields); + + const referer = { + reachedTop: refererInfo.reachedTop ?? null, + referer: refererUrl ?? null, + numIframes: refererInfo.numIframes ?? null, + stack: refererInfo.stack ?? null + }; + + return Object.values(referer).some(value => value != null) ? referer : null; +} + +function extractTcf(gdprConsent) { + if (!gdprConsent) { + return null; + } + + const tcf = { + consentString: gdprConsent.consentString ?? null, + gdprApplies: gdprConsent.gdprApplies ?? null, + addtlConsent: gdprConsent.addtlConsent ?? null + }; + const missingTcfFields = []; + + if (tcf.consentString == null) missingTcfFields.push('consentString'); + if (tcf.gdprApplies == null) missingTcfFields.push('gdprApplies'); + if (tcf.addtlConsent == null) missingTcfFields.push('addtlConsent'); + + logMissingFields('tcf', missingTcfFields); + + return tcf; +} + +function removeSchainFromFirstPartyData(firstPartyData) { + if (!firstPartyData?.source?.ext?.schain) { + return; + } + + delete firstPartyData.source.ext.schain; + + if (!Object.keys(firstPartyData.source.ext).length) { + delete firstPartyData.source.ext; + } + + if (!Object.keys(firstPartyData.source).length) { + delete firstPartyData.source; + } +} + +function extractFirstPartyData(ortb2) { + if (!ortb2) { + return null; + } + + const firstPartyData = deepClone(ortb2); + + removeSchainFromFirstPartyData(firstPartyData); + + if (firstPartyData.site) { + delete firstPartyData.site; + } + + return Object.keys(firstPartyData).length ? firstPartyData : null; +} + +function buildLegacyPayload(validBidRequests, bidderRequest) { + const payload = { + bids: validBidRequests, + version: VERSION + }; + + const referer = extractReferer(bidderRequest?.refererInfo); + if (referer) { + payload.referer = referer; + } + + const tcf = extractTcf(bidderRequest?.gdprConsent); + if (tcf) { + payload.tcf = tcf; + } + + const schain = bidderRequest?.ortb2?.source?.ext?.schain; + if (schain) { + payload.schain = schain; + } + + const firstPartyData = extractFirstPartyData(bidderRequest?.ortb2); + if (firstPartyData) { + payload.firstPartyData = firstPartyData; + } + + return payload; +} + +function buildOrtbRequest(validBidRequests, bidderRequest, endpoint) { + const ortbRequest = converter.toORTB({ + bidderRequest, + bidRequests: validBidRequests, + }); + + return { + method: 'POST', + url: endpoint, + data: ortbRequest, + ortb: true + }; +} export const spec = { code: BIDDER_CODE, gvlid: IAB_GVL_ID, isBidRequestValid: function ({ bidder, params = {} } = {}) { - // id parameter checks + const bidderName = bidder || BIDDER_CODE; + if (!params.id) { - logError(`${bidder}: Parameter 'id' missing`); + logError(`${bidderName}: Parameter 'id' missing`); + return false; + } + + if (typeof params.id !== 'string') { + logError(`${bidderName}: Parameter 'id' needs to be a string`); return false; - } else if (typeof params.id !== 'string') { - logError(`${bidder}: Parameter 'id' needs to be a string`); + } + + if (params.dimension && typeof params.dimension !== 'string') { + logError(`${bidderName}: Parameter 'dimension' needs to be a string`); return false; } - // dimension parameter checks - if (!params.dimension) { - logError(`${bidder}: Required parameter 'dimension' missing`); + + if (params.endpoint !== undefined && !isValidEndpointUrl(params.endpoint)) { + logError(`${bidderName}: Parameter 'endpoint' needs to be a valid HTTPS URL`); return false; - } else if (typeof params.dimension !== 'string') { - logError(`${bidder}: Parameter 'dimension' needs to be a string`); + } + + if (params.ortb !== undefined && typeof params.ortb !== 'boolean') { + logError(`${bidderName}: Parameter 'ortb' needs to be a boolean`); return false; } @@ -34,49 +231,65 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - const payload = { - bids: validBidRequests, - // TODO: please do not send internal data structures over the network - referer: bidderRequest.refererInfo.legacy, - version: VERSION - }; - const payloadString = JSON.stringify(payload); - - return { - method: 'POST', - url: ENDPOINT_URL, - data: payloadString - }; + const groups = {}; + + for (const bid of validBidRequests) { + const endpoint = resolveEndpoint(bid.params.endpoint); + const ortb = bid.params.ortb === true; + const key = `${endpoint}::${ortb}`; + + if (!groups[key]) { + groups[key] = { endpoint, ortb, bids: [] }; + } + + groups[key].bids.push(bid); + } + + return Object.values(groups).map(({ endpoint, ortb, bids }) => { + if (ortb) { + // ORTB mode uses an object payload and is interpreted via converter.fromORTB. + return buildOrtbRequest(bids, bidderRequest, endpoint); + } + + // Legacy mode uses a stringified JSON payload. + const payload = buildLegacyPayload(bids, bidderRequest); + + return { + method: 'POST', + url: endpoint, + data: JSON.stringify(payload) + }; + }); }, interpretResponse: function (serverResponse, request) { - const bids = serverResponse.body; + if (request?.ortb) { + return converter.fromORTB({ response: serverResponse?.body, request: request.data }).bids ?? []; + } - const bidResponses = []; + const bids = Array.isArray(serverResponse?.body) ? serverResponse.body : []; - bids.forEach(bid => { - const b = { + return bids.map((bid) => { + const responseBid = { ad: bid.ad, requestId: bid.requestId, cpm: bid.cpm, width: bid.width, height: bid.height, creativeId: bid.creativeId, - currency: bid.currency || DEFAULT_CURRENCY, - netRevenue: bid.netRevenue || DEFAULT_NETREVENUE, - ttl: bid.ttl || DEFAULT_TTL, + currency: bid.currency ?? DEFAULT_CURRENCY, + netRevenue: bid.netRevenue ?? DEFAULT_NETREVENUE, + ttl: bid.ttl ?? DEFAULT_TTL, meta: {}, }; if (bid.aDomains && bid.aDomains.length) { - b.meta.advertiserDomains = bid.aDomains; + responseBid.meta.advertiserDomains = bid.aDomains; } - bidResponses.push(b); + return responseBid; }); - - return bidResponses; }, -} +}; registerBidder(spec); diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 3766499b0e..d162cbbf84 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { config } from 'src/config.js'; import { spec } from 'modules/ipromBidAdapter.js'; describe('iPROM Adapter', function () { @@ -6,6 +7,7 @@ describe('iPROM Adapter', function () { let bidderRequest; beforeEach(function () { + config.resetConfig(); bidRequests = [ { bidder: 'iprom', @@ -29,26 +31,27 @@ describe('iPROM Adapter', function () { bidderRequest = { timeout: 3000, refererInfo: { - legacy: { - referer: 'https://adserver.si/index.html', - reachedTop: true, - numIframes: 1, - stack: [ - 'https://adserver.si/index.html', - 'https://adserver.si/iframe1.html', - ] - } + reachedTop: true, + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ], + topmostLocation: 'https://adserver.si/index.html', } } }); + afterEach(function () { + config.resetConfig(); + }); + describe('validating bids', function () { - it('should accept valid bid', function () { + it('should accept valid bid with only id', function () { const validBid = { bidder: 'iprom', params: { - id: '1234', - dimension: '300x250', + id: '1234' }, }; @@ -68,11 +71,12 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - it('should reject bid if missing dimension', function () { + it('should reject bid if dimension is not a string', function () { const invalidBid = { bidder: 'iprom', params: { id: '1234', + dimension: 404, } }; @@ -81,12 +85,11 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - it('should reject bid if dimension is not a string', function () { + it('should reject bid if missing id', function () { const invalidBid = { bidder: 'iprom', params: { - id: '1234', - dimension: 404, + dimension: '300x250', } }; @@ -95,10 +98,11 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - it('should reject bid if missing id', function () { + it('should reject bid if id is not a string', function () { const invalidBid = { bidder: 'iprom', params: { + id: 1234, dimension: '300x250', } }; @@ -108,12 +112,40 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - it('should reject bid if id is not a string', function () { + it('should reject bid if endpoint param is not a valid URL', function () { const invalidBid = { bidder: 'iprom', params: { - id: 1234, - dimension: '300x250', + id: '1234', + endpoint: 'not-a-valid-url', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should accept bid if endpoint param is a valid URL', function () { + const validBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 'https://custom.iprom.net/programmatic', + } + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject bid if ortb param is not a boolean', function () { + const invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + ortb: 'true', } }; @@ -121,36 +153,353 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); + + it('should accept bid if ortb param is true', function () { + const validBid = { + bidder: 'iprom', + params: { + id: '1234', + ortb: true, + } + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); }); describe('building requests', function () { it('should go to correct endpoint', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(1); + expect(requests[0].method).to.exist; + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.exist; + expect(requests[0].url).to.equal('https://core.iprom.net/programmatic'); + }); - expect(request.method).to.exist; - expect(request.method).to.equal('POST'); - expect(request.url).to.exist; - expect(request.url).to.equal('https://core.iprom.net/programmatic'); + it('should use custom endpoint from ad unit params with legacy format', function () { + const bidRequestsWithCustomEndpoint = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic' } + })); + const requests = spec.buildRequests(bidRequestsWithCustomEndpoint, bidderRequest); + + expect(requests[0].url).to.equal('https://global.iprom.net/programmatic'); + expect(requests[0].ortb).to.be.undefined; + expect(requests[0].data).to.be.a('string'); + }); + + it('should use ORTB format when ortb param is true', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].url).to.equal('https://core.iprom.net/programmatic'); + expect(requests[0].ortb).to.equal(true); + expect(requests[0].data).to.be.an('object'); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.bids).to.be.undefined; + }); + + it('should include id and dimension in imp.ext.bidder for ORTB requests', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.imp[0].ext.bidder).to.deep.equal({ + id: '1234', + dimension: '300x250' + }); }); - it('should add referer info', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + it('should include only id in imp.ext.bidder when dimension is not set', function () { + const bidRequestsWithOrtb = [{ + ...bidRequests[0], + params: { + id: '1234', + ortb: true + } + }]; + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.imp[0].ext.bidder).to.deep.equal({ + id: '1234' + }); + }); - expect(requestparse.referer).to.exist; - expect(requestparse.referer.referer).to.equal('https://adserver.si/index.html'); + it('should use custom endpoint with ORTB format when both endpoint and ortb params are set', function () { + const bidRequestsWithBoth = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic', ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithBoth, bidderRequest); + + expect(requests[0].url).to.equal('https://global.iprom.net/programmatic'); + expect(requests[0].ortb).to.equal(true); + expect(requests[0].data).to.be.an('object'); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.bids).to.be.undefined; + }); + + it('should include schain in ORTB request when present in bidderRequest.ortb2', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic', ortb: true } + })); + const bidderRequestWithGlobalSchain = { + ...bidderRequest, + ortb2: { + source: { + ext: { + schain + } + } + } + }; + + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequestWithGlobalSchain); + + expect(requests[0].data.source.ext.schain).to.deep.equal(schain); + }); + + it('should include referer info in ORTB request site.ext when present', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.site.ext.reachedTop).to.equal(true); + expect(requests[0].data.site.ext.numIframes).to.equal(1); + expect(requests[0].data.site.ext.stack).to.deep.equal([ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ]); + }); + + it('should not add referer ext fields to ORTB request when refererInfo is missing', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const bidderRequestWithoutReferer = { ...bidderRequest, refererInfo: undefined }; + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequestWithoutReferer); + + expect(requests[0].data.site?.ext?.reachedTop).to.be.undefined; + expect(requests[0].data.site?.ext?.numIframes).to.be.undefined; + expect(requests[0].data.site?.ext?.stack).to.be.undefined; + }); + + it('should group bids with different endpoints into separate requests', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, endpoint: 'https://endpoint1.iprom.net/programmatic' } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, endpoint: 'https://endpoint2.iprom.net/programmatic' } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(2); + const urls = requests.map(r => r.url); + expect(urls).to.include('https://endpoint1.iprom.net/programmatic'); + expect(urls).to.include('https://endpoint2.iprom.net/programmatic'); + }); + + it('should group bids with same endpoint but different ortb into separate requests', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, ortb: true } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, ortb: false } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(2); + + const ortbRequest = requests.find(r => r.ortb === true); + const legacyRequest = requests.find(r => r.ortb === undefined); + + expect(ortbRequest).to.exist; + expect(ortbRequest.data).to.be.an('object'); + expect(ortbRequest.data.imp).to.be.an('array').with.lengthOf(1); + + expect(legacyRequest).to.exist; + expect(legacyRequest.data).to.be.a('string'); + }); + + it('should group bids with same endpoint and ortb into a single request', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, ortb: true } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, ortb: true } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(2); + }); + + it('should add only selected referer info', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); + + expect(requestparse.referer).to.deep.equal({ + reachedTop: true, + referer: 'https://adserver.si/index.html', + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ] + }); + expect(requestparse.referer.canonicalUrl).to.be.undefined; + expect(requestparse.referer.legacy).to.be.undefined; }); it('should add adapter version', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.version).to.exist; }); + it('should add TCF data', function () { + const bidderRequestWithTcf = { + ...bidderRequest, + gdprConsent: { + consentString: 'consent-string', + gdprApplies: true, + addtlConsent: 'addtl-consent' + } + }; + const requests = spec.buildRequests(bidRequests, bidderRequestWithTcf); + const requestparse = JSON.parse(requests[0].data); + + expect(requestparse.tcf).to.deep.equal({ + consentString: 'consent-string', + gdprApplies: true, + addtlConsent: 'addtl-consent' + }); + }); + + it('should add schain data', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + const bidderRequestWithSchain = { + ...bidderRequest, + ortb2: { + source: { + ext: { + schain + } + } + } + }; + const requests = spec.buildRequests(bidRequests, bidderRequestWithSchain); + const requestparse = JSON.parse(requests[0].data); + + expect(requestparse.schain).to.deep.equal(schain); + }); + + it('should keep schain only at top level', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + const bidderRequestWithFpdAndSchain = { + ...bidderRequest, + ortb2: { + source: { + ext: { + schain + } + }, + site: { + domain: 'adserver.si' + } + } + }; + const requests = spec.buildRequests(bidRequests, bidderRequestWithFpdAndSchain); + const requestparse = JSON.parse(requests[0].data); + + expect(requestparse.schain).to.deep.equal(schain); + expect(requestparse.firstPartyData).to.be.undefined; + }); + + it('should add first party data', function () { + const firstPartyData = { + site: { + domain: 'adserver.si', + page: 'https://adserver.si/index.html' + }, + user: { + data: [{ + name: 'taxonomy', + segment: [{ id: 'segment-id' }] + }] + } + }; + const bidderRequestWithFpd = { + ...bidderRequest, + ortb2: firstPartyData + }; + const requests = spec.buildRequests(bidRequests, bidderRequestWithFpd); + const requestparse = JSON.parse(requests[0].data); + + expect(requestparse.firstPartyData).to.deep.equal({ + user: { + data: [{ + name: 'taxonomy', + segment: [{ id: 'segment-id' }] + }] + } + }); + }); + it('should contain id and dimension', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.bids[0].params.id).to.equal('1234'); expect(requestparse.bids[0].params.dimension).to.equal('300x250'); @@ -172,8 +521,8 @@ describe('iPROM Adapter', function () { ] }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const bids = spec.interpretResponse(serverResponse, request); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, requests[0]); expect(bids).to.be.lengthOf(1); expect(bids[0].requestId).to.equal('29a72b151f7bd3'); @@ -184,13 +533,82 @@ describe('iPROM Adapter', function () { expect(bids[0].meta.advertiserDomains).to.deep.equal(['https://example.com']); }); + it('should preserve false netRevenue and zero ttl from response', function () { + const serverResponse = { + body: [{ + requestId: '29a72b151f7bd3', + cpm: 0.5, + width: 300, + height: 250, + creativeId: 1234, + ad: '