forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dsaControl module: Reject bids without meta.dsa when required (prebid…
…#10982) * dsaControl - reject bids without meta.dsa when required * ortbConverter: always set meta.dsa * dsaControl: reject bids whose DSA rendering method disagrees with the request
- Loading branch information
Showing
8 changed files
with
269 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import {config} from '../src/config.js'; | ||
import {auctionManager} from '../src/auctionManager.js'; | ||
import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; | ||
import CONSTANTS from '../src/constants.json'; | ||
import {getHook} from '../src/hook.js'; | ||
import {logInfo, logWarn} from '../src/utils.js'; | ||
|
||
let expiryHandle; | ||
let dsaAuctions = {}; | ||
|
||
export const addBidResponseHook = timedBidResponseHook('dsa', function (fn, adUnitCode, bid, reject) { | ||
if (!dsaAuctions.hasOwnProperty(bid.auctionId)) { | ||
dsaAuctions[bid.auctionId] = auctionManager.index.getAuction(bid)?.getFPD?.()?.global?.regs?.ext?.dsa; | ||
} | ||
const dsaRequest = dsaAuctions[bid.auctionId]; | ||
let rejectReason; | ||
if (dsaRequest) { | ||
if (!bid.meta?.dsa) { | ||
if (dsaRequest.dsarequired === 1) { | ||
// request says dsa is supported; response does not have dsa info; warn about it | ||
logWarn(`dsaControl: ${CONSTANTS.REJECTION_REASON.DSA_REQUIRED}; will still be accepted as regs.ext.dsa.dsarequired = 1`, bid); | ||
} else if ([2, 3].includes(dsaRequest.dsarequired)) { | ||
// request says dsa is required; response does not have dsa info; reject it | ||
rejectReason = CONSTANTS.REJECTION_REASON.DSA_REQUIRED; | ||
} | ||
} else { | ||
if (dsaRequest.pubrender === 0 && bid.meta.dsa.adrender === 0) { | ||
// request says publisher can't render; response says advertiser won't; reject it | ||
rejectReason = CONSTANTS.REJECTION_REASON.DSA_MISMATCH; | ||
} else if (dsaRequest.pubrender === 2 && bid.meta.dsa.adrender === 1) { | ||
// request says publisher will render; response says advertiser will; reject it | ||
rejectReason = CONSTANTS.REJECTION_REASON.DSA_MISMATCH; | ||
} | ||
} | ||
} | ||
if (rejectReason) { | ||
reject(rejectReason); | ||
} else { | ||
return fn.call(this, adUnitCode, bid, reject); | ||
} | ||
}); | ||
|
||
function toggleHooks(enabled) { | ||
if (enabled && expiryHandle == null) { | ||
getHook('addBidResponse').before(addBidResponseHook); | ||
expiryHandle = auctionManager.onExpiry(auction => { | ||
delete dsaAuctions[auction.getAuctionId()]; | ||
}); | ||
logInfo('dsaControl: DSA bid validation is enabled') | ||
} else if (!enabled && expiryHandle != null) { | ||
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); | ||
expiryHandle(); | ||
expiryHandle = null; | ||
logInfo('dsaControl: DSA bid validation is disabled') | ||
} | ||
} | ||
|
||
export function reset() { | ||
toggleHooks(false); | ||
dsaAuctions = {}; | ||
} | ||
|
||
toggleHooks(true); | ||
|
||
config.getConfig('consentManagement', (cfg) => { | ||
toggleHooks(cfg.consentManagement?.dsa?.validateBids ?? true); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import {addBidResponseHook, setMetaDsa, reset} from '../../../modules/dsaControl.js'; | ||
import CONSTANTS from 'src/constants.json'; | ||
import {auctionManager} from '../../../src/auctionManager.js'; | ||
import {AuctionIndex} from '../../../src/auctionIndex.js'; | ||
|
||
describe('DSA transparency', () => { | ||
let sandbox; | ||
beforeEach(() => { | ||
sandbox = sinon.sandbox.create(); | ||
}); | ||
afterEach(() => { | ||
sandbox.restore(); | ||
reset(); | ||
}); | ||
|
||
describe('addBidResponseHook', () => { | ||
const auctionId = 'auction-id'; | ||
let bid, auction, fpd, next, reject; | ||
beforeEach(() => { | ||
next = sinon.stub(); | ||
reject = sinon.stub(); | ||
fpd = {}; | ||
bid = { | ||
auctionId | ||
} | ||
auction = { | ||
getAuctionId: () => auctionId, | ||
getFPD: () => ({global: fpd}) | ||
} | ||
sandbox.stub(auctionManager, 'index').get(() => new AuctionIndex(() => [auction])); | ||
}); | ||
|
||
function expectRejection(reason) { | ||
addBidResponseHook(next, 'adUnit', bid, reject); | ||
sinon.assert.calledWith(reject, reason); | ||
sinon.assert.notCalled(next); | ||
} | ||
|
||
function expectAcceptance() { | ||
addBidResponseHook(next, 'adUnit', bid, reject); | ||
sinon.assert.notCalled(reject); | ||
sinon.assert.calledWith(next, 'adUnit', bid, reject); | ||
} | ||
|
||
[2, 3].forEach(required => { | ||
describe(`when regs.ext.dsa.dsarequired is ${required} (required)`, () => { | ||
beforeEach(() => { | ||
fpd = { | ||
regs: {ext: {dsa: {dsarequired: required}}} | ||
}; | ||
}); | ||
|
||
it('should reject bids that have no meta.dsa', () => { | ||
expectRejection(CONSTANTS.REJECTION_REASON.DSA_REQUIRED); | ||
}); | ||
|
||
it('should accept bids that do', () => { | ||
bid.meta = {dsa: {}}; | ||
expectAcceptance(); | ||
}); | ||
|
||
describe('and pubrender = 0 (rendering by publisher not supported)', () => { | ||
beforeEach(() => { | ||
fpd.regs.ext.dsa.pubrender = 0; | ||
}); | ||
|
||
it('should reject bids with adrender = 0 (advertiser will not render)', () => { | ||
bid.meta = {dsa: {adrender: 0}}; | ||
expectRejection(CONSTANTS.REJECTION_REASON.DSA_MISMATCH); | ||
}); | ||
|
||
it('should accept bids with adrender = 1 (advertiser will render)', () => { | ||
bid.meta = {dsa: {adrender: 1}}; | ||
expectAcceptance(); | ||
}); | ||
}); | ||
describe('and pubrender = 2 (publisher will render)', () => { | ||
beforeEach(() => { | ||
fpd.regs.ext.dsa.pubrender = 2; | ||
}); | ||
|
||
it('should reject bids with adrender = 1 (advertiser will render)', () => { | ||
bid.meta = {dsa: {adrender: 1}}; | ||
expectRejection(CONSTANTS.REJECTION_REASON.DSA_MISMATCH); | ||
}); | ||
|
||
it('should accept bids with adrender = 0 (advertiser will not render)', () => { | ||
bid.meta = {dsa: {adrender: 0}}; | ||
expectAcceptance(); | ||
}) | ||
}) | ||
}); | ||
}); | ||
[undefined, 'garbage', 0, 1].forEach(required => { | ||
describe(`when regs.ext.dsa.dsarequired is ${required}`, () => { | ||
beforeEach(() => { | ||
if (required != null) { | ||
fpd = { | ||
regs: {ext: {dsa: {dsarequired: required}}} | ||
} | ||
} | ||
}); | ||
|
||
it('should accept bids regardless of their meta.dsa', () => { | ||
addBidResponseHook(next, 'adUnit', bid, reject); | ||
sinon.assert.notCalled(reject); | ||
sinon.assert.calledWith(next, 'adUnit', bid, reject); | ||
}) | ||
}) | ||
}) | ||
it('should accept bids regardless of dsa when "required" any other value') | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {DEFAULT_PROCESSORS} from '../../../libraries/ortbConverter/processors/default.js'; | ||
import {BID_RESPONSE} from '../../../src/pbjsORTB.js'; | ||
|
||
describe('common processors', () => { | ||
describe('bid response properties', () => { | ||
const responseProps = DEFAULT_PROCESSORS[BID_RESPONSE].props.fn; | ||
let context; | ||
|
||
beforeEach(() => { | ||
context = { | ||
ortbResponse: {} | ||
} | ||
}) | ||
|
||
describe('meta.dsa', () => { | ||
const MOCK_DSA = {transparency: 'info'}; | ||
it('is not set if bid has no meta.dsa', () => { | ||
const resp = {}; | ||
responseProps(resp, {}, context); | ||
expect(resp.meta?.dsa).to.not.exist; | ||
}); | ||
it('is set to ext.dsa otherwise', () => { | ||
const resp = {}; | ||
responseProps(resp, {ext: {dsa: MOCK_DSA}}, context); | ||
expect(resp.meta.dsa).to.eql(MOCK_DSA); | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters