diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 5f94ec3dea4..cfad8fce966 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isArray, logError, logInfo, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepClone, isArray, logError, logInfo, mergeDeep, isEmpty, isPlainObject, isNumber, isStr} from '../src/utils.js'; import {getOrigin} from '../libraries/getOrigin/index.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -18,6 +18,12 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; const GVLID = 16; +const DSA_ATTRIBUTES = [ + { name: 'dsarequired', 'min': 0, 'max': 3 }, + { name: 'pubrender', 'min': 0, 'max': 2 }, + { name: 'datatopub', 'min': 0, 'max': 2 } +]; + // Codes defined by OpenRTB Native Ads 1.1 specification export const OPENRTB = { NATIVE: { @@ -95,6 +101,17 @@ export const spec = { } }); + const dsa = deepAccess(ortb2Params, 'regs.ext.dsa'); + if (validateDSA(dsa)) { + mergeDeep(request, { + regs: { + ext: { + dsa + } + } + }); + } + let computedEndpointUrl = ENDPOINT_URL; if (bidderRequest.fledgeEnabled) { @@ -133,7 +150,13 @@ export const spec = { } else { interpretedBid = interpretBannerBid(serverBid); } - if (serverBid.ext) interpretedBid.ext = serverBid.ext; + + if (serverBid.ext) { + interpretedBid.ext = deepClone(serverBid.ext); + if (serverBid.ext.dsa) { + interpretedBid.meta = Object.assign({}, interpretedBid.meta, { dsa: serverBid.ext.dsa }); + } + } bids.push(interpretedBid); }); @@ -518,3 +541,27 @@ function interpretNativeAd(adm) { }); return result; } + +/** + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + * + * @param {object} dsa + * @returns {boolean} whether dsa object contains valid attributes values + */ +function validateDSA(dsa) { + if (isEmpty(dsa) || !isPlainObject(dsa)) return false; + + return DSA_ATTRIBUTES.reduce((prev, attr) => { + const dsaEntry = dsa[attr.name]; + return prev && ( + !dsa.hasOwnProperty(attr.name) || + (isNumber(dsaEntry) && dsaEntry >= attr.min && dsaEntry <= attr.max) + ) + }, true) && + (!dsa.hasOwnProperty('transparency') || + (isArray(dsa.transparency) && dsa.transparency.every( + v => isPlainObject(v) && isStr(v.domain) && v.domain && isArray(v.dsaparams) && + v.dsaparams.every(x => isNumber(x)) + )) + ) +} diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 0b944dcb077..77b746b9b69 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { OPENRTB, spec } from 'modules/rtbhouseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { mergeDeep } from '../../../src/utils'; describe('RTBHouseAdapter', () => { const adapter = newBidder(spec); @@ -304,6 +305,152 @@ describe('RTBHouseAdapter', () => { expect(data.user).to.nested.include({'ext.data': 'some user data'}); }); + context('DSA', () => { + const validDSAObject = { + 'dsarequired': 3, + 'pubrender': 0, + 'datatopub': 2, + 'transparency': [ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ] + }; + const invalidDSAObjects = [ + -1, + 0, + '', + 'x', + true, + [], + [1], + {}, + { + 'dsarequired': -1 + }, + { + 'pubrender': -1 + }, + { + 'datatopub': -1 + }, + { + 'dsarequired': 4 + }, + { + 'pubrender': 3 + }, + { + 'datatopub': 3 + }, + { + 'dsarequired': '1' + }, + { + 'pubrender': '1' + }, + { + 'datatopub': '1' + }, + { + 'transparency': '1' + }, + { + 'transparency': 2 + }, + { + 'transparency': [ + 1, 2 + ] + }, + { + 'transparency': [ + { + domain: '', + dsaparams: [] + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: null + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: [1, '2'] + } + ] + }, + ]; + let bidRequest; + + beforeEach(() => { + bidRequest = Object.assign([], bidRequests); + }); + + it('should add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa', function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: validDSAObject + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.have.nested.property('regs.ext.dsa'); + expect(data.regs.ext.dsa.dsarequired).to.equal(3); + expect(data.regs.ext.dsa.pubrender).to.equal(0); + expect(data.regs.ext.dsa.datatopub).to.equal(2); + expect(data.regs.ext.dsa.transparency).to.deep.equal([ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ]); + }); + + invalidDSAObjects.forEach((invalidDSA, index) => { + it(`should not add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa; test# ${index}`, function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: invalidDSA + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.not.have.nested.property('regs.ext.dsa'); + }); + }); + }); + context('FLEDGE', function() { afterEach(function () { config.resetConfig(); @@ -563,17 +710,20 @@ describe('RTBHouseAdapter', () => { }); describe('interpretResponse', function () { - let response = [{ - 'id': 'bidder_imp_identifier', - 'impid': '552b8922e28f27', - 'price': 0.5, - 'adid': 'Ad_Identifier', - 'adm': '', - 'adomain': ['rtbhouse.com'], - 'cid': 'Ad_Identifier', - 'w': 300, - 'h': 250 - }]; + let response; + beforeEach(() => { + response = [{ + 'id': 'bidder_imp_identifier', + 'impid': '552b8922e28f27', + 'price': 0.5, + 'adid': 'Ad_Identifier', + 'adm': '', + 'adomain': ['rtbhouse.com'], + 'cid': 'Ad_Identifier', + 'w': 300, + 'h': 250 + }]; + }); let fledgeResponse = { 'id': 'bid-identifier', @@ -638,6 +788,51 @@ describe('RTBHouseAdapter', () => { }); }); + context('when the response contains DSA object', function () { + it('should get correct bid response', function () { + const dsa = { + 'dsa': { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': [{ + 'domain': 'dsp1domain.com', + 'dsaparams': [1, 2] + }], + 'adrender': 1 + } + }; + mergeDeep(response[0], { ext: dsa }); + + const expectedResponse = [ + { + 'requestId': '552b8922e28f27', + 'cpm': 0.5, + 'creativeId': 29681110, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['rtbhouse.com'], + ...dsa + }, + 'netRevenue': true, + ext: { ...dsa } + } + ]; + let bidderRequest; + let result = spec.interpretResponse({body: response}, {bidderRequest}); + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.have.nested.property('meta.dsa'); + expect(result[0]).to.have.nested.property('ext.dsa'); + expect(result[0].meta.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + expect(result[0].ext.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + }); + }); + describe('native', () => { const adm = { native: {