diff --git a/libraries/ortbConverter/processors/default.js b/libraries/ortbConverter/processors/default.js index d92a51daba2..9d916b87172 100644 --- a/libraries/ortbConverter/processors/default.js +++ b/libraries/ortbConverter/processors/default.js @@ -100,6 +100,13 @@ export const DEFAULT_PROCESSORS = { if (bid.ext?.dsa) { bidResponse.meta.dsa = bid.ext.dsa; } + if (bid.cat) { + bidResponse.meta.primaryCatId = bid.cat[0]; + bidResponse.meta.secondaryCatIds = bid.cat.slice(1); + } + if (bid.attr) { + bidResponse.meta.attr = bid.attr; + } } } } diff --git a/libraries/vastTrackers/vastTrackers.js b/libraries/vastTrackers/vastTrackers.js index b4ae98aba57..f414a65a18c 100644 --- a/libraries/vastTrackers/vastTrackers.js +++ b/libraries/vastTrackers/vastTrackers.js @@ -7,19 +7,24 @@ import {activityParams} from '../../src/activities/activityParams.js'; const vastTrackers = []; -addBidResponse.before(function (next, adUnitcode, bidResponse, reject) { +export function reset() { + vastTrackers.length = 0; +} + +export function addTrackersToResponse(next, adUnitcode, bidResponse, reject) { if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { const vastTrackers = getVastTrackers(bidResponse); if (vastTrackers) { bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); const impTrackers = vastTrackers.get('impressions'); if (impTrackers) { - bidResponse.vastImpUrl = [].concat(impTrackers).concat(bidResponse.vastImpUrl).filter(t => t); + bidResponse.vastImpUrl = [].concat([...impTrackers]).concat(bidResponse.vastImpUrl).filter(t => t); } } } next(adUnitcode, bidResponse, reject); -}); +} +addBidResponse.before(addTrackersToResponse); export function registerVastTrackers(moduleType, moduleName, trackerFn) { if (typeof trackerFn === 'function') { diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js index fbb4300cb24..5c3409f4780 100644 --- a/libraries/vidazooUtils/bidderUtils.js +++ b/libraries/vidazooUtils/bidderUtils.js @@ -466,7 +466,7 @@ export function createBuildRequestsFn(createRequestDomain, createUniqueRequestDa return function buildRequests(validBidRequests, bidderRequest) { const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); + const bidderTimeout = bidderRequest.timeout || config.getConfig('bidderTimeout'); const singleRequestMode = allowSingleRequest && config.getConfig(`${bidderCode}.singleRequest`); diff --git a/modules/bidResponseFilter/index.js b/modules/bidResponseFilter/index.js new file mode 100644 index 00000000000..3ace8108d2b --- /dev/null +++ b/modules/bidResponseFilter/index.js @@ -0,0 +1,40 @@ +import { auctionManager } from '../../src/auctionManager.js'; +import { config } from '../../src/config.js'; +import { getHook } from '../../src/hook.js'; + +export const MODULE_NAME = 'bidResponseFilter'; +export const BID_CATEGORY_REJECTION_REASON = 'Category is not allowed'; +export const BID_ADV_DOMAINS_REJECTION_REASON = 'Adv domain is not allowed'; +export const BID_ATTR_REJECTION_REASON = 'Attr is not allowed'; + +function init() { + getHook('addBidResponse').before(addBidResponseHook); +}; + +export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctionManager.index) { + const {bcat = [], badv = []} = index.getOrtb2(bid) || {}; + const battr = index.getBidRequest(bid)?.ortb2Imp[bid.mediaType]?.battr || index.getAdUnit(bid)?.ortb2Imp[bid.mediaType]?.battr || []; + const moduleConfig = config.getConfig(MODULE_NAME); + + const catConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.cat || {})}; + const advConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.adv || {})}; + const attrConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.attr || {})}; + + const { primaryCatId, secondaryCatIds = [], advertiserDomains = [], attr: metaAttr } = bid.meta || {}; + + // checking if bid fulfills ortb2 fields rules + if ((catConfig.enforce && bcat.some(category => [primaryCatId, ...secondaryCatIds].includes(category))) || + (catConfig.blockUnknown && !primaryCatId)) { + reject(BID_CATEGORY_REJECTION_REASON); + } else if ((advConfig.enforce && badv.some(domain => advertiserDomains.includes(domain))) || + (advConfig.blockUnknown && !advertiserDomains.length)) { + reject(BID_ADV_DOMAINS_REJECTION_REASON); + } else if ((attrConfig.enforce && battr.includes(metaAttr)) || + (attrConfig.blockUnknown && !metaAttr)) { + reject(BID_ATTR_REJECTION_REASON); + } else { + return next(adUnitCode, bid, reject); + } +} + +init(); diff --git a/modules/digitalMatterBidAdapter.js b/modules/digitalMatterBidAdapter.js index c6663434d1e..77df1af3886 100644 --- a/modules/digitalMatterBidAdapter.js +++ b/modules/digitalMatterBidAdapter.js @@ -5,10 +5,12 @@ import {BANNER} from '../src/mediaTypes.js'; import {hasPurpose1Consent} from '../src/utils/gdpr.js'; const BIDDER_CODE = 'digitalMatter'; +const GVLID = 1345; const ENDPOINT_URL = 'https://adx.digitalmatter.services/' export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER], aliases: ['dichange', 'digitalmatter'], bidParameters: ['accountId', 'siteId'], diff --git a/modules/smartytechBidAdapter.md b/modules/smartytechBidAdapter.md index 9df57ddbde7..53b246e4cab 100644 --- a/modules/smartytechBidAdapter.md +++ b/modules/smartytechBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: SmartyTech Bid Adapter Module Type: Bidder Adapter -Maintainer: info@adpartner.pro +Maintainer: info@fusify.io ``` # Description diff --git a/modules/smootBidAdapter.js b/modules/smootBidAdapter.js new file mode 100644 index 00000000000..cd55dc5f253 --- /dev/null +++ b/modules/smootBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'smoot'; +const AD_URL = 'https://endpoint1.smoot.ai/pbjs'; +const SYNC_URL = 'https://usync.smxconv.com'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/smootBidAdapter.md b/modules/smootBidAdapter.md new file mode 100644 index 00000000000..322584f19ea --- /dev/null +++ b/modules/smootBidAdapter.md @@ -0,0 +1,80 @@ +# Overview + +``` +Module Name: Smoot Bidder Adapter +Module Type: Smoot Bidder Adapter +Maintainer: it.ops@smoot.ai +``` + +# Description + +Connects to Smoot exchange for bids. +Smoot bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters + +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'smoot', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'smoot', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'smoot', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/src/auctionIndex.js b/src/auctionIndex.js index afae2089518..d0b8355352a 100644 --- a/src/auctionIndex.js +++ b/src/auctionIndex.js @@ -65,6 +65,9 @@ export function AuctionIndex(getAuctions) { .flatMap(ber => ber.bids) .find(br => br && br.bidId === requestId); } + }, + getOrtb2(bid) { + return this.getBidderRequest(bid)?.ortb2 || this.getAuction(bid)?.getFPD()?.global?.ortb2 } }); } diff --git a/src/prebid.js b/src/prebid.js index 18a9127a793..975e4b4517b 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -100,6 +100,22 @@ function validateSizes(sizes, targLength) { return cleanSizes; } +export function setBattrForAdUnit(adUnit, mediaType) { + const ortb2Imp = adUnit.ortb2Imp || {}; + const mediaTypes = adUnit.mediaTypes || {}; + + if (ortb2Imp[mediaType]?.battr && mediaTypes[mediaType]?.battr && (ortb2Imp[mediaType]?.battr !== mediaTypes[mediaType]?.battr)) { + logWarn(`Ad unit ${adUnit.code} specifies conflicting ortb2Imp.${mediaType}.battr and mediaTypes.${mediaType}.battr, the latter will be ignored`, adUnit); + } + + const battr = ortb2Imp[mediaType]?.battr || mediaTypes[mediaType]?.battr; + + if (battr != null) { + deepSetValue(adUnit, `ortb2Imp.${mediaType}.battr`, battr); + deepSetValue(adUnit, `mediaTypes.${mediaType}.battr`, battr); + } +} + function validateBannerMediaType(adUnit) { const validatedAdUnit = deepClone(adUnit); const banner = validatedAdUnit.mediaTypes.banner; @@ -112,6 +128,7 @@ function validateBannerMediaType(adUnit) { logError('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.'); delete validatedAdUnit.mediaTypes.banner } + setBattrForAdUnit(validatedAdUnit, 'banner'); return validatedAdUnit; } @@ -135,6 +152,7 @@ function validateVideoMediaType(adUnit) { } } validateOrtbVideoFields(validatedAdUnit); + setBattrForAdUnit(validatedAdUnit, 'video'); return validatedAdUnit; } @@ -184,6 +202,7 @@ function validateNativeMediaType(adUnit) { logError('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.'); delete validatedAdUnit.mediaTypes.native.icon.sizes; } + setBattrForAdUnit(validatedAdUnit, 'native'); return validatedAdUnit; } diff --git a/test/spec/libraries/vastTrackers_spec.js b/test/spec/libraries/vastTrackers_spec.js index 3849ea75b02..c336eec0321 100644 --- a/test/spec/libraries/vastTrackers_spec.js +++ b/test/spec/libraries/vastTrackers_spec.js @@ -1,17 +1,27 @@ -import {addImpUrlToTrackers, getVastTrackers, insertVastTrackers, registerVastTrackers} from 'libraries/vastTrackers/vastTrackers.js'; +import { + addImpUrlToTrackers, + addTrackersToResponse, + getVastTrackers, + insertVastTrackers, + registerVastTrackers, + reset +} from 'libraries/vastTrackers/vastTrackers.js'; import {MODULE_TYPE_ANALYTICS} from '../../../src/activities/modules.js'; describe('vast trackers', () => { + beforeEach(() => { + registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', function(bidResponse) { + return [ + {'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`} + ]; + }); + }) + afterEach(() => { + reset(); + }); + it('insert into tracker list', function() { - let trackers = getVastTrackers({'cpm': 1.0}); - if (!trackers || !trackers.get('impressions')) { - registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', function(bidResponse) { - return [ - {'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`} - ]; - }); - } - trackers = getVastTrackers({'cpm': 1.0}); + const trackers = getVastTrackers({'cpm': 1.0}); expect(trackers).to.be.a('map'); expect(trackers.get('impressions')).to.exists; expect(trackers.get('impressions').has('https://vasttracking.mydomain.com/vast?cpm=1')).to.be.true; @@ -30,4 +40,17 @@ describe('vast trackers', () => { expect(trackers.get('impressions')).to.exists; expect(trackers.get('impressions').has('imptracker.com')).to.be.true; }); + + if (FEATURES.VIDEO) { + it('should add trackers to bid response', () => { + const bidResponse = { + mediaType: 'video', + cpm: 1 + } + addTrackersToResponse(sinon.stub(), 'au', bidResponse); + expect(bidResponse.vastImpUrl).to.eql([ + 'https://vasttracking.mydomain.com/vast?cpm=1' + ]) + }); + } }) diff --git a/test/spec/modules/ads_interactiveBidAdapter_spec.js b/test/spec/modules/ads_interactiveBidAdapter_spec.js index 93fd94a0f2d..f33e24fd25d 100644 --- a/test/spec/modules/ads_interactiveBidAdapter_spec.js +++ b/test/spec/modules/ads_interactiveBidAdapter_spec.js @@ -137,6 +137,7 @@ describe('AdsInteractiveBidAdapter', function () { expect(data).to.have.all.keys( 'deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', diff --git a/test/spec/modules/bidResponseFilter_spec.js b/test/spec/modules/bidResponseFilter_spec.js new file mode 100644 index 00000000000..3990cd3feb3 --- /dev/null +++ b/test/spec/modules/bidResponseFilter_spec.js @@ -0,0 +1,135 @@ +import { BID_ADV_DOMAINS_REJECTION_REASON, BID_ATTR_REJECTION_REASON, BID_CATEGORY_REJECTION_REASON, MODULE_NAME, PUBLISHER_FILTER_REJECTION_REASON, addBidResponseHook } from '../../../modules/bidResponseFilter'; +import { config } from '../../../src/config'; + +describe('bidResponseFilter', () => { + let mockAuctionIndex + beforeEach(() => { + config.resetConfig(); + mockAuctionIndex = { + getBidRequest: () => {}, + getAdUnit: () => {} + }; + }); + + it('should pass the bid after successful ortb2 rules validation', () => { + const call = sinon.stub(); + + mockAuctionIndex.getOrtb2 = () => ({ + badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'EXAMPLE-CAT-ID', + attr: 'attr' + } + }; + + addBidResponseHook(call, 'adcode', bid, () => {}, mockAuctionIndex); + sinon.assert.calledOnce(call); + }); + + it('should reject the bid after failed ortb2 cat rule validation', () => { + const reject = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'BANNED_CAT1', + attr: 'attr' + } + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex); + sinon.assert.calledWith(reject, BID_CATEGORY_REJECTION_REASON); + }); + + it('should reject the bid after failed ortb2 adv domains rule validation', () => { + const rejection = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'VALID_CAT', + attr: 'attr' + } + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + addBidResponseHook(call, 'adcode', bid, rejection, mockAuctionIndex); + sinon.assert.calledWith(rejection, BID_ADV_DOMAINS_REJECTION_REASON); + }); + + it('should reject the bid after failed ortb2 attr rule validation', () => { + const reject = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: 'VALID_CAT', + attr: 'BANNED_ATTR' + }, + mediaType: 'video' + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + mockAuctionIndex.getBidRequest = () => ({ + ortb2Imp: { + video: { + battr: 'BANNED_ATTR' + } + } + }) + + addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex); + sinon.assert.calledWith(reject, BID_ATTR_REJECTION_REASON); + }); + + it('should omit the validation if the flag is set to false', () => { + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: 'BANNED_CAT1', + attr: 'valid_attr' + } + }; + + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + config.setConfig({[MODULE_NAME]: {cat: {enforce: false}}}); + + addBidResponseHook(call, 'adcode', bid, () => {}, mockAuctionIndex); + sinon.assert.calledOnce(call); + }); + + it('should allow bid for unknown flag set to false', () => { + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: undefined, + attr: 'valid_attr' + } + }; + + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + config.setConfig({[MODULE_NAME]: {cat: {blockUnknown: false}}}); + + addBidResponseHook(call, 'adcode', bid, () => {}); + sinon.assert.calledOnce(call); + }); +}) diff --git a/test/spec/modules/dsp_genieeBidAdapter_spec.js b/test/spec/modules/dsp_genieeBidAdapter_spec.js index 94ec1011fbf..6b2286a5fe5 100644 --- a/test/spec/modules/dsp_genieeBidAdapter_spec.js +++ b/test/spec/modules/dsp_genieeBidAdapter_spec.js @@ -121,7 +121,9 @@ describe('Geniee adapter tests', () => { currency: 'JPY', mediaType: 'banner', meta: { - advertiserDomains: ['geniee.co.jp'] + advertiserDomains: ['geniee.co.jp'], + primaryCatId: 'IAB1', + secondaryCatIds: [] }, netRevenue: true, requestId: 'bid-id', diff --git a/test/spec/modules/smootBidAdapter_spec.js b/test/spec/modules/smootBidAdapter_spec.js new file mode 100644 index 00000000000..cf72b41b348 --- /dev/null +++ b/test/spec/modules/smootBidAdapter_spec.js @@ -0,0 +1,595 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/smootBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'smoot'; + +describe('SmootBidAdapter', function () { + const userIdAsEids = [ + { + source: 'test.org', + uids: [ + { + id: '01**********', + atype: 1, + ext: { + third: '01***********', + }, + }, + ], + }, + ]; + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + }, + }, + params: { + placementId: 'testBanner', + }, + userIdAsEids, + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + }, + }, + params: { + placementId: 'testVideo', + }, + userIdAsEids, + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true, + }, + body: { + required: true, + }, + icon: { + required: true, + size: [64, 64], + }, + }, + }, + }, + params: { + placementId: 'testNative', + }, + userIdAsEids, + }, + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + }, + }, + params: {}, + }; + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: + 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {}, + }, + refererInfo: { + referer: 'https://test.com', + }, + timeout: 500, + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf([ + 'testBanner', + 'testVideo', + 'testNative', + ]); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + }, + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids, + }, + ]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.endpointId).to.be.oneOf([ + 'testBanner', + 'testVideo', + 'testNative', + ]); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('network'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal( + bidderRequest.gdprConsent.consentString + ); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8], + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }); + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2 = bidderRequest.ortb2 || {}; + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [ + { + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234, + }, + }, + ], + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys( + 'requestId', + 'cpm', + 'width', + 'height', + 'ad', + 'ttl', + 'creativeId', + 'netRevenue', + 'currency', + 'dealId', + 'mediaType', + 'meta' + ); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta) + .to.be.an('object') + .that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [ + { + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234, + }, + }, + ], + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys( + 'requestId', + 'cpm', + 'vastUrl', + 'ttl', + 'creativeId', + 'netRevenue', + 'currency', + 'dealId', + 'mediaType', + 'meta' + ); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta) + .to.be.an('object') + .that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [ + { + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234, + }, + }, + ], + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys( + 'requestId', + 'cpm', + 'ttl', + 'creativeId', + 'netRevenue', + 'currency', + 'mediaType', + 'native', + 'meta' + ); + expect(dataItem.native).to.have.keys( + 'clickUrl', + 'impressionTrackers', + 'title', + 'image' + ); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not + .empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta) + .to.be.an('object') + .that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [ + { + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + }, + ], + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [ + { + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + }, + ], + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [ + { + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }, + ], + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [ + { + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + }, + ], + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Should return array of objects with proper sync config , include GDPR', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + { + consentString: 'ALL', + gdprApplies: true, + }, + {} + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0' + ); + }); + it('Should return array of objects with proper sync config , include CCPA', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + {}, + { + consentString: '1---', + } + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&ccpa_consent=1---&coppa=0' + ); + }); + it('Should return array of objects with proper sync config , include GPP', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + {}, + {}, + { + gppString: 'abc123', + applicableSections: [8], + } + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0' + ); + }); + }); +}); diff --git a/test/spec/ortbConverter/banner_spec.js b/test/spec/ortbConverter/banner_spec.js index 0f6686283a1..a54dbcd0847 100644 --- a/test/spec/ortbConverter/banner_spec.js +++ b/test/spec/ortbConverter/banner_spec.js @@ -126,6 +126,16 @@ describe('pbjs -> ortb banner conversion', () => { expect(imp.banner.someParam).to.eql('someValue'); }); + it('should keep ortb2Imp.banner.battr', () => { + const imp = { + banner: { + battr: 'battr' + } + }; + fillBannerImp(imp, {mediaTypes: {banner: {sizes: [1, 2]}}}, {}); + expect(imp.banner.battr).to.eql('battr'); + }); + it('does nothing if context.mediaType is set but is not BANNER', () => { const imp = {}; fillBannerImp(imp, {mediaTypes: {banner: {sizes: [1, 2]}}}, {mediaType: VIDEO}); diff --git a/test/spec/ortbConverter/native_spec.js b/test/spec/ortbConverter/native_spec.js index 56c733817cd..8ff1f9254fb 100644 --- a/test/spec/ortbConverter/native_spec.js +++ b/test/spec/ortbConverter/native_spec.js @@ -46,6 +46,16 @@ describe('pbjs -> ortb native requests', () => { expect(imp.native.something).to.eql('orother') }); + it('should keep ortb2Imp.native.battr', () => { + const imp = { + native: { + battr: 'battr' + } + }; + fillNativeImp(imp, {mediaTypes: {native: {sizes: [1, 2]}}}, {}); + expect(imp.native.battr).to.eql('battr'); + }); + it('should do nothing if there are no assets', () => { const imp = {}; fillNativeImp(imp, {nativeOrtbRequest: {assets: []}}, {}); diff --git a/test/spec/ortbConverter/video_spec.js b/test/spec/ortbConverter/video_spec.js index ab4034bb60a..9a3675beb6d 100644 --- a/test/spec/ortbConverter/video_spec.js +++ b/test/spec/ortbConverter/video_spec.js @@ -122,6 +122,16 @@ describe('pbjs -> ortb video conversion', () => { expect(imp.video.someParam).to.eql('someValue'); }); + it('should keep ortb2Imp.video.battr', () => { + const imp = { + video: { + battr: 'battr' + } + }; + fillVideoImp(imp, {mediaTypes: {video: {sizes: [1, 2]}}}, {}); + expect(imp.video.battr).to.eql('battr'); + }); + it('does nothing is context.mediaType is set but is not VIDEO', () => { const imp = {}; fillVideoImp(imp, {mediaTypes: {video: {playerSize: [[1, 2]]}}}, {mediaType: BANNER}); diff --git a/test/spec/unit/core/auctionIndex_spec.js b/test/spec/unit/core/auctionIndex_spec.js index df29ed1a6cb..cc93e66adcf 100644 --- a/test/spec/unit/core/auctionIndex_spec.js +++ b/test/spec/unit/core/auctionIndex_spec.js @@ -3,11 +3,14 @@ import {AuctionIndex} from '../../../../src/auctionIndex.js'; describe('auction index', () => { let index, auctions; - function mockAuction(id, adUnits, bidderRequests) { + function mockAuction(id, adUnits, bidderRequests, ortb2) { return { getAuctionId() { return id }, getAdUnits() { return adUnits; }, - getBidRequests() { return bidderRequests; } + getBidRequests() { return bidderRequests; }, + getFPD() { + return { global: { ortb2 } } + } } } @@ -126,4 +129,27 @@ describe('auction index', () => { }); }) }); + + describe('getOrtb2', () => { + let bidderRequests, adUnits = []; + beforeEach(() => { + bidderRequests = [ + {bidderRequestId: 'ber1', ortb2: {}, bids: [{bidId: 'b1', adUnitId: 'au1'}, {}]}, + {bidderRequestId: 'ber2', bids: [{bidId: 'b2', adUnitId: 'au2'}]} + ] + auctions = [ + mockAuction('a1', [adUnits[0]], [bidderRequests[0], {}]), + mockAuction('a2', [adUnits[1]], [bidderRequests[1]], {ortb2Field: true}) + ] + }); + it('should return ortb2 for bid if exists on bidder request', () => { + const ortb2 = index.getOrtb2({bidderRequestId: 'ber1'}); + expect(ortb2).to.be.a('object'); + }) + + it('should return ortb2 from auction if does not exist on bidder request', () => { + const ortb2 = index.getOrtb2({bidderRequestId: 'ber2', auctionId: 'a2'}); + expect(ortb2).to.be.deep.equals({ortb2Field: true}); + }) + }) }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 69e608e0d65..60363ad359d 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -28,6 +28,7 @@ import {generateUUID} from '../../../src/utils.js'; import {getCreativeRenderer} from '../../../src/creativeRenderers.js'; import {BID_STATUS, EVENTS, GRANULARITY_OPTIONS, PB_LOCATOR, TARGETING_KEYS} from 'src/constants.js'; import {getBidToRender} from '../../../src/adRendering.js'; +import { setBattrForAdUnit } from '../../../src/prebid.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -3745,4 +3746,71 @@ describe('Unit: Prebid Module', function () { expect(auctionManager.getBidsReceived().length).to.equal(0); }); }); + + describe('setBattrForAdUnit', () => { + it('should set copy battr to both places', () => { + const adUnit = { + ortb2Imp: { + video: { + battr: 'banned attribute' + } + }, + mediaTypes: { + video: {} + } + } + + setBattrForAdUnit(adUnit, 'video'); + + expect(adUnit.mediaTypes.video.battr).to.deep.equal('banned attribute'); + expect(adUnit.ortb2Imp.video.battr).to.deep.equal('banned attribute'); + + const adUnit2 = { + mediaTypes: { + video: { + battr: 'banned attribute' + } + }, + ortb2Imp: { + video: {} + } + } + + setBattrForAdUnit(adUnit2, 'video'); + + expect(adUnit2.ortb2Imp.video.battr).to.deep.equal('banned attribute'); + expect(adUnit2.mediaTypes.video.battr).to.deep.equal('banned attribute'); + }) + + it('should log warn if both are specified and differ from eachother', () => { + let spyLogWarn = sinon.spy(utils, 'logWarn'); + const adUnit = { + mediaTypes: { + native: { + battr: 'banned attribute' + } + }, + ortb2Imp: { + native: { + battr: 'banned attribute 2' + } + } + } + setBattrForAdUnit(adUnit, 'native'); + sinon.assert.calledOnce(spyLogWarn); + spyLogWarn.resetHistory(); + utils.logWarn.restore(); + }) + + it('should not copy for undefined battr', () => { + const adUnit = { + mediaTypes: { + native: {} + } + } + setBattrForAdUnit(adUnit, 'native'); + expect(adUnit.mediaTypes.native).to.deep.equal({}); + expect(adUnit.mediaTypes.ortb2Imp).to.not.exist; + }) + }) });