From 4584dbb5b75738e5a109afae00156a731af732bc Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Mon, 4 Aug 2025 20:57:17 +0530 Subject: [PATCH 01/11] Prebid Core: Cosnent Handler reset functionality --- libraries/consentManagement/cmUtils.ts | 2 ++ src/consentHandler.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 88dfffef9cd..4a4a23e4f48 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -168,6 +168,8 @@ export function configParser( buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; } + consentDataHandler.reset(); + logInfo(`${displayName} consentManagement module has been disabled...`) } return function getConsentConfig(config: { [key: string]: CMConfig }) { diff --git a/src/consentHandler.ts b/src/consentHandler.ts index b69f9df0a1a..701197f962b 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -108,7 +108,10 @@ export class ConsentHandler { } getConsentData(): T { - return this.#data; + if (this.#enabled) { + return this.#data; + } + return null; } get hash() { From 5411509a45cd4269d5d6294bb997e7cc90f20bf7 Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Tue, 5 Aug 2025 13:57:36 +0530 Subject: [PATCH 02/11] Prebid Consent Management: Add reset --- libraries/consentManagement/cmUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 4a4a23e4f48..62399f02e51 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -167,9 +167,10 @@ export function configParser( getHook('requestBids').getHooks({hook: requestBidsHook}).remove(); buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; + // Reseting respective consent data handler + consentDataHandler.reset(); + logInfo(`${displayName} consentManagement module has been diactivated...`) } - consentDataHandler.reset(); - logInfo(`${displayName} consentManagement module has been disabled...`) } return function getConsentConfig(config: { [key: string]: CMConfig }) { From 5f95c4bbf6e484849ce49a1b720edded332791e9 Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Tue, 5 Aug 2025 17:25:36 +0530 Subject: [PATCH 03/11] Consent Management Reset: Remove add event listener if it is listening already --- libraries/consentManagement/cmUtils.ts | 1 + modules/consentManagementGpp.ts | 4 ++ modules/consentManagementTcf.ts | 4 ++ src/consentHandler.ts | 66 +++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 62399f02e51..cccb148b2b9 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -168,6 +168,7 @@ export function configParser( buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; // Reseting respective consent data handler + consentDataHandler.removeCmpEventListener(); consentDataHandler.reset(); logInfo(`${displayName} consentManagement module has been diactivated...`) } diff --git a/modules/consentManagementGpp.ts b/modules/consentManagementGpp.ts index 905cffda213..0aadf346fa5 100644 --- a/modules/consentManagementGpp.ts +++ b/modules/consentManagementGpp.ts @@ -101,6 +101,7 @@ export class GPPClient { logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`); } this.initialized = true; + gppDataHandler.setCmpApi(this.cmp); this.cmp({ command: 'addEventListener', callback: (event, success) => { @@ -109,6 +110,9 @@ export class GPPClient { } else if (event?.pingData?.cmpStatus === 'error') { this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { + if(event?.listenerId !== null && event?.listenerId !== undefined){ + gppDataHandler.setCmpListenerId(event?.listenerId); + } this.#resolve(this.updateConsent(event.pingData)); } // NOTE: according to https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md, diff --git a/modules/consentManagementTcf.ts b/modules/consentManagementTcf.ts index e693132c8af..9357db98060 100644 --- a/modules/consentManagementTcf.ts +++ b/modules/consentManagementTcf.ts @@ -87,6 +87,9 @@ function lookupIabConsent(setProvisionalConsent) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { try { + if(tcfData.listenerId !== null && tcfData.listenerId !== undefined){ + gdprDataHandler.setCmpListenerId(tcfData.listenerId); + } gdprDataHandler.setConsentData(parseConsentData(tcfData)); resolve(); } catch (e) { @@ -113,6 +116,7 @@ function lookupIabConsent(setProvisionalConsent) { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); } + gdprDataHandler.setCmpApi(cmp); cmp({ command: 'addEventListener', callback: cmpResponseCallback diff --git a/src/consentHandler.ts b/src/consentHandler.ts index 701197f962b..c31ac140e63 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -42,9 +42,11 @@ export class ConsentHandler { #ready; #dirty = true; #hash; + #listenerId: string | null = null; + #cmpApi: any = null; generatedTime: number; hashFields; - + constructor() { this.reset(); } @@ -55,6 +57,36 @@ export class ConsentHandler { this.#defer.resolve(data); } + /** + * Set CMP API reference + * @param cmpApi - CMP API reference + */ + setCmpApi(cmpApi: any) { + this.#cmpApi = cmpApi; + } + + /** + * Get CMP API reference + */ + getCmpApi() { + return this.#cmpApi; + } + + /** + * Set CMP listener ID + * @param listenerId - Unique identifier for the CMP listener + */ + setCmpListenerId(listenerId: string) { + this.#listenerId = listenerId; + } + + /** + * Get CMP listener ID + */ + getCmpListenerId() { + return this.#listenerId; + } + /** * reset this handler (mainly for tests) */ @@ -124,6 +156,11 @@ export class ConsentHandler { } class UspConsentHandler extends ConsentHandler> { + /** + * Remove CMP event listener using CMP API + * This will be just a placeholder for now as we don't have any removeEventListener for USP + */ + removeCmpEventListener() {} getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -136,6 +173,20 @@ class UspConsentHandler extends ConsentHandler> { hashFields = ['gdprApplies', 'consentString'] + /** + * Remove CMP event listener using CMP API + */ + removeCmpEventListener() { + if (this.getCmpApi() && this.getCmpListenerId()) { + const apiVersion = this.getConsentData()?.apiVersion || 2; + this.getCmpApi()('removeEventListener', apiVersion, function (data: boolean, success: boolean) { + if (success) { + this.setCmpApi(null); + this.setCmpListenerId(null); + } + }, this.getCmpListenerId()); + } + } getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { @@ -151,6 +202,19 @@ class GdprConsentHandler extends ConsentHandler> { hashFields = ['applicableSections', 'gppString']; + /** + * Remove CMP event listener using CMP API + */ + removeCmpEventListener() { + if (this.getCmpApi() && this.getCmpListenerId()) { + this.getCmpApi()('removeEventListener', function (data: boolean, success: boolean) { + if (data) { + this.setCmpApi(null); + this.setCmpListenerId(null); + } + }, this.getCmpListenerId()); + } + } getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { From 2cf4ef515e73294c0b077fe19f566f051e92c031 Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Tue, 12 Aug 2025 11:44:48 +0530 Subject: [PATCH 04/11] Consent management changes working --- modules/consentManagementGpp.ts | 9 ++-- src/consentHandler.ts | 73 +++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/modules/consentManagementGpp.ts b/modules/consentManagementGpp.ts index 0aadf346fa5..5aca00bc0c5 100644 --- a/modules/consentManagementGpp.ts +++ b/modules/consentManagementGpp.ts @@ -109,10 +109,7 @@ export class GPPClient { this.#reject(new GPPError('Received error response from CMP', event)); } else if (event?.pingData?.cmpStatus === 'error') { this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); - } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { - if(event?.listenerId !== null && event?.listenerId !== undefined){ - gppDataHandler.setCmpListenerId(event?.listenerId); - } + } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { this.#resolve(this.updateConsent(event.pingData)); } // NOTE: according to https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md, @@ -124,6 +121,10 @@ export class GPPClient { if (gppDataHandler.getConsentData() != null && event?.pingData != null && !this.isCMPReady(event.pingData)) { gppDataHandler.setConsentData(null); } + + if(event?.listenerId !== null && event?.listenerId !== undefined){ + gppDataHandler.setCmpListenerId(event?.listenerId); + } } }); } diff --git a/src/consentHandler.ts b/src/consentHandler.ts index c31ac140e63..b20d49a155c 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -42,11 +42,11 @@ export class ConsentHandler { #ready; #dirty = true; #hash; - #listenerId: string | null = null; + #listenerId: number | undefined = undefined; #cmpApi: any = null; generatedTime: number; hashFields; - + constructor() { this.reset(); } @@ -76,7 +76,7 @@ export class ConsentHandler { * Set CMP listener ID * @param listenerId - Unique identifier for the CMP listener */ - setCmpListenerId(listenerId: string) { + setCmpListenerId(listenerId: number | undefined) { this.#listenerId = listenerId; } @@ -87,6 +87,13 @@ export class ConsentHandler { return this.#listenerId; } + resetCmpApis(success: boolean) { + if (success) { + this.#cmpApi = null; + this.#listenerId = undefined; + } + } + /** * reset this handler (mainly for tests) */ @@ -148,11 +155,33 @@ export class ConsentHandler { get hash() { if (this.#dirty) { - this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data)) + this.#hash = cyrb53Hash( + JSON.stringify( + this.#data && this.hashFields ? this.hashFields.map((f) => this.#data[f]) : this.#data + ) + ); this.#dirty = false; } return this.#hash; } + + addApiVersionToParams(params: any) {} + + // Base class defines the algorithm structure + removeCmpEventListener() { + if (this.getCmpApi() && this.getCmpListenerId() !== undefined && this.getCmpListenerId() !== null) { + const params = { + command: "removeEventListener", + callback: this.resetCmpApis.bind(this), + parameter: this.getCmpListenerId(), + }; + + // Call the method that subclasses will override + this.addApiVersionToParams(params); + + this.getCmpApi()(params); + } + } } class UspConsentHandler extends ConsentHandler> { @@ -172,49 +201,31 @@ class UspConsentHandler extends ConsentHandler> { - hashFields = ['gdprApplies', 'consentString'] + hashFields = ["gdprApplies", "consentString"]; /** * Remove CMP event listener using CMP API */ - removeCmpEventListener() { - if (this.getCmpApi() && this.getCmpListenerId()) { - const apiVersion = this.getConsentData()?.apiVersion || 2; - this.getCmpApi()('removeEventListener', apiVersion, function (data: boolean, success: boolean) { - if (success) { - this.setCmpApi(null); - this.setCmpListenerId(null); - } - }, this.getCmpListenerId()); - } + addApiVersionToParams(params: any) { + const apiVersion = this.getConsentData()?.apiVersion || 2; + params.apiVersion = apiVersion; } getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { return { gdprApplies: consentData.gdprApplies as boolean, - consentStringSize: (isStr(consentData.vendorData.tcString)) ? consentData.vendorData.tcString.length : 0, + consentStringSize: isStr(consentData.vendorData.tcString) + ? consentData.vendorData.tcString.length + : 0, generatedAt: this.generatedTime, - apiVersion: consentData.apiVersion - } + apiVersion: consentData.apiVersion, + }; } } } class GppConsentHandler extends ConsentHandler> { hashFields = ['applicableSections', 'gppString']; - /** - * Remove CMP event listener using CMP API - */ - removeCmpEventListener() { - if (this.getCmpApi() && this.getCmpListenerId()) { - this.getCmpApi()('removeEventListener', function (data: boolean, success: boolean) { - if (data) { - this.setCmpApi(null); - this.setCmpListenerId(null); - } - }, this.getCmpListenerId()); - } - } getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { From 1e233533430a3304351253856b7a22c8c5f46647 Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Wed, 13 Aug 2025 16:40:11 +0530 Subject: [PATCH 05/11] Consent Management: add enabled flag before enabling the module. Provided backward compatibility --- libraries/consentManagement/cmUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index cccb148b2b9..d9b193b4d9e 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -112,6 +112,12 @@ export interface BaseCMConfig { * for the user to interact with the CMP. */ actionTimeout?: number; + /** + * Flag to enable or disable the consent management module. + * When set to false, the module will be reset and disabled. + * Defaults to true when not specified. + */ + enabled?: boolean; } export interface IABCMConfig { @@ -181,6 +187,14 @@ export function configParser( reset(); return {}; } + + // Check if module is explicitly disabled + if (cmConfig?.enabled === false) { + logInfo(msg(`config enabled is set to false, disabling consent manager module`)); + reset(); + return {}; + } + let cmpHandler; if (isStr(cmConfig.cmpApi)) { cmpHandler = cmConfig.cmpApi; From 0b8ffedf136e89052c66aece3c88730cf357e4ed Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Wed, 13 Aug 2025 16:41:44 +0530 Subject: [PATCH 06/11] Consent Management: logInfo to logWarn --- libraries/consentManagement/cmUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index d9b193b4d9e..4628fea5101 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -190,7 +190,7 @@ export function configParser( // Check if module is explicitly disabled if (cmConfig?.enabled === false) { - logInfo(msg(`config enabled is set to false, disabling consent manager module`)); + logWarn(msg(`config enabled is set to false, disabling consent manager module`)); reset(); return {}; } From 37e2b3bc14b1999cb776dd896add2e9b70a93cbc Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Thu, 21 Aug 2025 12:14:19 +0530 Subject: [PATCH 07/11] Consent Manegement reset fix --- libraries/consentManagement/cmUtils.ts | 15 +-- modules/consentManagementGpp.ts | 6 +- modules/consentManagementTcf.ts | 4 +- src/consentHandler.ts | 5 - test/spec/libraries/cmUtils_spec.js | 75 ++++++++++++++- test/spec/unit/core/consentHandler_spec.js | 106 +++++++++++++++++++++ 6 files changed, 194 insertions(+), 17 deletions(-) diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 4628fea5101..438ea9aa61d 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -173,13 +173,16 @@ export function configParser( getHook('requestBids').getHooks({hook: requestBidsHook}).remove(); buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; - // Reseting respective consent data handler - consentDataHandler.removeCmpEventListener(); - consentDataHandler.reset(); logInfo(`${displayName} consentManagement module has been diactivated...`) } } + function resetConsentDataHandler() { + reset(); + consentDataHandler.removeCmpEventListener(); + consentDataHandler.reset(); + } + return function getConsentConfig(config: { [key: string]: CMConfig }) { const cmConfig = config?.[namespace]; if (!cmConfig || typeof cmConfig !== 'object') { @@ -187,14 +190,14 @@ export function configParser( reset(); return {}; } - + // Check if module is explicitly disabled if (cmConfig?.enabled === false) { logWarn(msg(`config enabled is set to false, disabling consent manager module`)); - reset(); + resetConsentDataHandler(); return {}; } - + let cmpHandler; if (isStr(cmConfig.cmpApi)) { cmpHandler = cmConfig.cmpApi; diff --git a/modules/consentManagementGpp.ts b/modules/consentManagementGpp.ts index 5aca00bc0c5..ae527959330 100644 --- a/modules/consentManagementGpp.ts +++ b/modules/consentManagementGpp.ts @@ -109,7 +109,7 @@ export class GPPClient { this.#reject(new GPPError('Received error response from CMP', event)); } else if (event?.pingData?.cmpStatus === 'error') { this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); - } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { + } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { this.#resolve(this.updateConsent(event.pingData)); } // NOTE: according to https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md, @@ -122,8 +122,8 @@ export class GPPClient { gppDataHandler.setConsentData(null); } - if(event?.listenerId !== null && event?.listenerId !== undefined){ - gppDataHandler.setCmpListenerId(event?.listenerId); + if (event?.listenerId !== null && event?.listenerId !== undefined) { + gppDataHandler.setCmpListenerId(event?.listenerId); } } }); diff --git a/modules/consentManagementTcf.ts b/modules/consentManagementTcf.ts index 9357db98060..01767bb3be8 100644 --- a/modules/consentManagementTcf.ts +++ b/modules/consentManagementTcf.ts @@ -87,8 +87,8 @@ function lookupIabConsent(setProvisionalConsent) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { try { - if(tcfData.listenerId !== null && tcfData.listenerId !== undefined){ - gdprDataHandler.setCmpListenerId(tcfData.listenerId); + if (tcfData.listenerId !== null && tcfData.listenerId !== undefined) { + gdprDataHandler.setCmpListenerId(tcfData.listenerId); } gdprDataHandler.setConsentData(parseConsentData(tcfData)); resolve(); diff --git a/src/consentHandler.ts b/src/consentHandler.ts index b20d49a155c..feba3f268c4 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -185,11 +185,6 @@ export class ConsentHandler { } class UspConsentHandler extends ConsentHandler> { - /** - * Remove CMP event listener using CMP API - * This will be just a placeholder for now as we don't have any removeEventListener for USP - */ - removeCmpEventListener() {} getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { diff --git a/test/spec/libraries/cmUtils_spec.js b/test/spec/libraries/cmUtils_spec.js index 7f9c2c932b3..99d515201f8 100644 --- a/test/spec/libraries/cmUtils_spec.js +++ b/test/spec/libraries/cmUtils_spec.js @@ -1,5 +1,5 @@ import * as utils from 'src/utils.js'; -import {lookupConsentData, consentManagementHook} from '../../../libraries/consentManagement/cmUtils.js'; +import {lookupConsentData, consentManagementHook, configParser} from '../../../libraries/consentManagement/cmUtils.js'; describe('consent management utils', () => { let sandbox, clock; @@ -226,4 +226,77 @@ describe('consent management utils', () => { }) }); }); + + describe('configParser', () => { + let namespace, displayName, consentDataHandler, parseConsentData, getNullConsent, cmpHandlers; + let getConsentConfig, resetConsentDataHandler; + + beforeEach(() => { + namespace = 'test'; + displayName = 'TEST'; + resetConsentDataHandler = sinon.stub(); + consentDataHandler = { + reset: sinon.stub(), + removeCmpEventListener: sinon.stub(), + getConsentData: sinon.stub(), + setConsentData: sinon.stub() + }; + parseConsentData = sinon.stub().callsFake(data => data); + getNullConsent = sinon.stub().returns({consent: null}); + cmpHandlers = { + iab: sinon.stub().returns(Promise.resolve()) + }; + + // Create a spy for resetConsentDataHandler to verify it's called + const configParserInstance = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers + }); + + getConsentConfig = configParserInstance; + }); + + it('should reset and return empty object when config is not defined', () => { + const result = getConsentConfig(); + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config not defined')); + }); + + it('should reset and return empty object when config is not an object', () => { + const result = getConsentConfig({[namespace]: 'not an object'}); + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config not defined')); + }); + + describe('when module is explicitly disabled', () => { + it('should reset consent data handler and return empty object when enabled is false', () => { + const result = getConsentConfig({[namespace]: {enabled: false}}); + + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config enabled is set to false')); + sinon.assert.called(consentDataHandler.removeCmpEventListener); + sinon.assert.called(consentDataHandler.reset); + }); + + it('should not reset consent data handler when enabled is true', () => { + getConsentConfig({[namespace]: {enabled: true, cmpApi: 'iab'}}); + + sinon.assert.notCalled(consentDataHandler.removeCmpEventListener); + sinon.assert.notCalled(consentDataHandler.reset); + }); + + it('should not reset consent data handler when enabled is not specified', () => { + getConsentConfig({[namespace]: {cmpApi: 'iab'}}); + + sinon.assert.notCalled(consentDataHandler.removeCmpEventListener); + sinon.assert.notCalled(consentDataHandler.reset); + }); + }); + + // Additional tests for other configParser functionality could be added here + }); }); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 1bcad3216ce..26635677933 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -85,6 +85,112 @@ describe('Consent data handler', () => { expect(handler.hash).to.eql(h1); }) }) + + describe('removeCmpEventListener', () => { + let cmpApiStub; + + beforeEach(() => { + cmpApiStub = sinon.stub(); + handler.setCmpApi(cmpApiStub); + }); + + it('should not call CMP API when no listener ID is set', () => { + handler.setCmpListenerId(undefined); + handler.removeCmpEventListener(); + sinon.assert.notCalled(cmpApiStub); + }); + + it('should not call CMP API when listener ID is null', () => { + handler.setCmpListenerId(null); + handler.removeCmpEventListener(); + sinon.assert.notCalled(cmpApiStub); + }); + + it('should not call CMP API when CMP API is not set', () => { + handler.setCmpApi(null); + handler.setCmpListenerId(123); + handler.removeCmpEventListener(); + sinon.assert.notCalled(cmpApiStub); + }); + + it('should call CMP API with correct parameters when both API and listener ID are set', () => { + const listenerId = 123; + handler.setCmpListenerId(listenerId); + + // Create spy for addApiVersionToParams + const addApiVersionToParamsSpy = sinon.spy(handler, 'addApiVersionToParams'); + + handler.removeCmpEventListener(); + + // Verify addApiVersionToParams was called with correct parameters + sinon.assert.calledOnce(addApiVersionToParamsSpy); + const params = addApiVersionToParamsSpy.firstCall.args[0]; + expect(params).to.have.property('command', 'removeEventListener'); + expect(params).to.have.property('parameter', listenerId); + expect(params.callback).to.be.a('function'); + + // Verify CMP API was called with the same parameters + sinon.assert.calledOnce(cmpApiStub); + sinon.assert.calledWith(cmpApiStub, params); + + // Clean up spy + addApiVersionToParamsSpy.restore(); + }); + + it('should reset CMP APIs when callback is called with success=true', () => { + const listenerId = 123; + handler.setCmpListenerId(listenerId); + + handler.removeCmpEventListener(); + + // Get the callback that was passed to the CMP API + const callback = cmpApiStub.firstCall.args[0].callback; + + // Call the callback with success=true + callback({}, true); + + // Verify CMP API and listener ID are reset + expect(handler.getCmpApi()).to.be.null; + expect(handler.getCmpListenerId()).to.be.undefined; + }); + + it('should not reset CMP APIs when callback is called with success=false', () => { + const listenerId = 123; + handler.setCmpListenerId(listenerId); + + handler.removeCmpEventListener(); + + // Get the callback that was passed to the CMP API + const callback = cmpApiStub.firstCall.args[0].callback; + + // Call the callback with success=false + callback(null, false); + + // Verify CMP API and listener ID are not reset + expect(handler.getCmpApi()).to.equal(cmpApiStub); + expect(handler.getCmpListenerId()).to.equal(listenerId); + }); + + it('should call addApiVersionToParams to allow subclasses to customize parameters', () => { + // Create a subclass that overrides addApiVersionToParams + class TestConsentHandler extends ConsentHandler { + addApiVersionToParams(params) { + params.version = 2; + } + } + + const testHandler = new TestConsentHandler(); + const testCmpApiStub = sinon.stub(); + testHandler.setCmpApi(testCmpApiStub); + testHandler.setCmpListenerId(456); + + testHandler.removeCmpEventListener(); + + // Verify CMP API was called with the customized parameters + sinon.assert.calledOnce(testCmpApiStub); + expect(testCmpApiStub.firstCall.args[0]).to.have.property('version', 2); + }); + }); }); describe('multiHandler', () => { From 9e654e5be831ef5a228428d33d815abc30c782bc Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Wed, 8 Oct 2025 18:13:36 +0530 Subject: [PATCH 08/11] Add gdpr test cases --- test/spec/modules/consentManagement_spec.js | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index a033c56ddcd..0c3aad51aad 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -736,6 +736,123 @@ describe('consentManagement', function () { expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); }); + + it('should set CMP listener ID when listenerId is provided in tcfData', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: 123 + }; + + const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpListenerIdSpy); + sinon.assert.calledWith(setCmpListenerIdSpy, 123); + + setCmpListenerIdSpy.restore(); + }); + + it('should not set CMP listener ID when listenerId is null', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: null + }; + + const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.notCalled(setCmpListenerIdSpy); + + setCmpListenerIdSpy.restore(); + }); + + it('should not set CMP listener ID when listenerId is undefined', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: undefined + }; + + const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.notCalled(setCmpListenerIdSpy); + + setCmpListenerIdSpy.restore(); + }); + + it('should set CMP listener ID when listenerId is 0 (valid listener ID)', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: 0 + }; + + const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpListenerIdSpy); + sinon.assert.calledWith(setCmpListenerIdSpy, 0); + + setCmpListenerIdSpy.restore(); + }); + + it('should set CMP API reference when CMP is found', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded' + }; + + const setCmpApiSpy = sinon.spy(gdprDataHandler, 'setCmpApi'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpApiSpy); + expect(setCmpApiSpy.getCall(0).args[0]).to.be.a('function'); + + setCmpApiSpy.restore(); + }); }); }); }); From c49ecb80d4b92ec955c5038e9427811073e45feb Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Tue, 14 Oct 2025 10:17:02 +0530 Subject: [PATCH 09/11] Move the cmp event listener removal functions to libraries --- libraries/cmp/cmpEventUtils.ts | 121 +++++++ libraries/consentManagement/cmUtils.ts | 13 +- modules/consentManagementGpp.ts | 26 +- modules/consentManagementTcf.ts | 26 +- src/consentHandler.ts | 61 ---- test/spec/libraries/cmUtils_spec.js | 69 +++- test/spec/libraries/cmp/cmpEventUtils_spec.js | 330 ++++++++++++++++++ test/spec/modules/consentManagement_spec.js | 20 +- test/spec/unit/core/consentHandler_spec.js | 106 ------ 9 files changed, 577 insertions(+), 195 deletions(-) create mode 100644 libraries/cmp/cmpEventUtils.ts create mode 100644 test/spec/libraries/cmp/cmpEventUtils_spec.js diff --git a/libraries/cmp/cmpEventUtils.ts b/libraries/cmp/cmpEventUtils.ts new file mode 100644 index 00000000000..4619e9605c9 --- /dev/null +++ b/libraries/cmp/cmpEventUtils.ts @@ -0,0 +1,121 @@ +/** + * Shared utilities for CMP event listener management + * Used by TCF and GPP consent management modules + */ + +import { logError, logInfo } from "../../src/utils.js"; + +export interface CmpEventManager { + cmpApi: any; + listenerId: number | undefined; + setCmpApi(cmpApi: any): void; + getCmpApi(): any; + setCmpListenerId(listenerId: number | undefined): void; + getCmpListenerId(): number | undefined; + removeCmpEventListener(): void; + resetCmpApis(): void; +} + +/** + * Base CMP event manager implementation + */ +export abstract class BaseCmpEventManager implements CmpEventManager { + cmpApi: any = null; + listenerId: number | undefined = undefined; + + setCmpApi(cmpApi: any): void { + this.cmpApi = cmpApi; + } + + getCmpApi(): any { + return this.cmpApi; + } + + setCmpListenerId(listenerId: number | undefined): void { + this.listenerId = listenerId; + } + + getCmpListenerId(): number | undefined { + return this.listenerId; + } + + resetCmpApis(): void { + this.cmpApi = null; + this.listenerId = undefined; + } + + /** + * Helper method to get base removal parameters + * Can be used by subclasses that need to remove event listeners + */ + protected getRemoveListenerParams(): Record | null { + const cmpApi = this.getCmpApi(); + const listenerId = this.getCmpListenerId(); + + // Comprehensive validation for all possible failure scenarios + if (cmpApi && typeof cmpApi === 'function' && listenerId !== undefined && listenerId !== null) { + return { + command: "removeEventListener", + callback: () => this.resetCmpApis(), + parameter: listenerId + }; + } + return null; + } + + /** + * Abstract method - each subclass implements its own removal logic + */ + abstract removeCmpEventListener(): void; +} + +/** + * TCF-specific CMP event manager + */ +export class TcfCmpEventManager extends BaseCmpEventManager { + private getConsentData: () => any; + + constructor(getConsentData?: () => any) { + super(); + this.getConsentData = getConsentData || (() => null); + } + + removeCmpEventListener(): void { + const params = this.getRemoveListenerParams(); + if (params) { + const consentData = this.getConsentData(); + params.apiVersion = consentData?.apiVersion || 2; + logInfo('Removing TCF CMP event listener'); + this.getCmpApi()(params); + } + } +} + +/** + * GPP-specific CMP event manager + * GPP doesn't require event listener removal, so this is empty + */ +export class GppCmpEventManager extends BaseCmpEventManager { + removeCmpEventListener(): void { + const params = this.getRemoveListenerParams(); + if (params) { + logInfo('Removing GPP CMP event listener'); + this.getCmpApi()(params); + } + } +} + +/** + * Factory function to create appropriate CMP event manager + */ +export function createCmpEventManager(type: 'tcf' | 'gpp', getConsentData?: () => any): CmpEventManager { + switch (type) { + case 'tcf': + return new TcfCmpEventManager(getConsentData); + case 'gpp': + return new GppCmpEventManager(); + default: + logError(`Unknown CMP type: ${type}`); + return null; + } +} diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 438ea9aa61d..d8012241fc7 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -142,6 +142,7 @@ export function configParser( parseConsentData, getNullConsent, cmpHandlers, + cmpEventCleanup, DEFAULT_CMP = 'iab', DEFAULT_CONSENT_TIMEOUT = 10000 } = {} as any @@ -173,14 +174,20 @@ export function configParser( getHook('requestBids').getHooks({hook: requestBidsHook}).remove(); buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; - logInfo(`${displayName} consentManagement module has been diactivated...`) + logInfo(`${displayName} consentManagement module has been deactivated...`); } } function resetConsentDataHandler() { reset(); - consentDataHandler.removeCmpEventListener(); - consentDataHandler.reset(); + // Call module-specific CMP event cleanup if provided + if (typeof cmpEventCleanup === 'function') { + try { + cmpEventCleanup(); + } catch (e) { + logError(`Error during CMP event cleanup for ${displayName}:`, e); + } + } } return function getConsentConfig(config: { [key: string]: CMConfig }) { diff --git a/modules/consentManagementGpp.ts b/modules/consentManagementGpp.ts index ae527959330..fcbcaeb664c 100644 --- a/modules/consentManagementGpp.ts +++ b/modules/consentManagementGpp.ts @@ -11,10 +11,14 @@ import {enrichFPD} from '../src/fpd/enrichment.js'; import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js'; import {PbPromise, defer} from '../src/utils/promise.js'; import {type CMConfig, configParser} from '../libraries/consentManagement/cmUtils.js'; +import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js'; import {CONSENT_GPP} from "../src/consentHandler.ts"; export let consentConfig = {} as any; +// CMP event manager instance for GPP +let gppCmpEventManager: CmpEventManager | null = null; + type RelevantCMPData = { applicableSections: number[] gppString: string; @@ -101,7 +105,13 @@ export class GPPClient { logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`); } this.initialized = true; - gppDataHandler.setCmpApi(this.cmp); + + // Initialize CMP event manager and set CMP API + if (!gppCmpEventManager) { + gppCmpEventManager = createCmpEventManager('gpp'); + } + gppCmpEventManager.setCmpApi(this.cmp); + this.cmp({ command: 'addEventListener', callback: (event, success) => { @@ -123,7 +133,7 @@ export class GPPClient { } if (event?.listenerId !== null && event?.listenerId !== undefined) { - gppDataHandler.setCmpListenerId(event?.listenerId); + gppCmpEventManager?.setCmpListenerId(event?.listenerId); } } }); @@ -223,13 +233,23 @@ export function resetConsentData() { GPPClient.INST = null; } +export function removeCmpListener() { + // Clean up CMP event listeners before resetting + if (gppCmpEventManager) { + gppCmpEventManager.removeCmpEventListener(); + gppCmpEventManager = null; + } + resetConsentData(); +} + const parseConfig = configParser({ namespace: 'gpp', displayName: 'GPP', consentDataHandler: gppDataHandler, parseConsentData, getNullConsent: () => toConsentData(null), - cmpHandlers: cmpCallMap + cmpHandlers: cmpCallMap, + cmpEventCleanup: removeCmpListener }); export function setConsentConfig(config) { diff --git a/modules/consentManagementTcf.ts b/modules/consentManagementTcf.ts index 01767bb3be8..673a2d6f269 100644 --- a/modules/consentManagementTcf.ts +++ b/modules/consentManagementTcf.ts @@ -11,6 +11,7 @@ import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; import {cmpClient} from '../libraries/cmp/cmpClient.js'; import {configParser} from '../libraries/consentManagement/cmUtils.js'; +import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js'; import {CONSENT_GDPR} from "../src/consentHandler.ts"; import type {CMConfig} from "../libraries/consentManagement/cmUtils.ts"; @@ -24,6 +25,9 @@ const cmpCallMap = { 'iab': lookupIabConsent, }; +// CMP event manager instance for TCF +export let tcfCmpEventManager: CmpEventManager | null = null; + /** * @see https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework * @see https://github.com/InteractiveAdvertisingBureau/iabtcf-es/tree/master/modules/core#iabtcfcore @@ -88,7 +92,7 @@ function lookupIabConsent(setProvisionalConsent) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { try { if (tcfData.listenerId !== null && tcfData.listenerId !== undefined) { - gdprDataHandler.setCmpListenerId(tcfData.listenerId); + tcfCmpEventManager?.setCmpListenerId(tcfData.listenerId); } gdprDataHandler.setConsentData(parseConsentData(tcfData)); resolve(); @@ -116,7 +120,12 @@ function lookupIabConsent(setProvisionalConsent) { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); } - gdprDataHandler.setCmpApi(cmp); + // Initialize CMP event manager and set CMP API + if (!tcfCmpEventManager) { + tcfCmpEventManager = createCmpEventManager('tcf', () => gdprDataHandler.getConsentData()); + } + tcfCmpEventManager.setCmpApi(cmp); + cmp({ command: 'addEventListener', callback: cmpResponseCallback @@ -163,14 +172,25 @@ export function resetConsentData() { gdprDataHandler.reset(); } +export function removeCmpListener() { + // Clean up CMP event listeners before resetting + if (tcfCmpEventManager) { + tcfCmpEventManager.removeCmpEventListener(); + tcfCmpEventManager = null; + } + resetConsentData(); +} + const parseConfig = configParser({ namespace: 'gdpr', displayName: 'TCF', consentDataHandler: gdprDataHandler, cmpHandlers: cmpCallMap, parseConsentData, - getNullConsent: () => toConsentData(null) + getNullConsent: () => toConsentData(null), + cmpEventCleanup: removeCmpListener } as any) + /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function */ diff --git a/src/consentHandler.ts b/src/consentHandler.ts index feba3f268c4..2691429e241 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -42,8 +42,6 @@ export class ConsentHandler { #ready; #dirty = true; #hash; - #listenerId: number | undefined = undefined; - #cmpApi: any = null; generatedTime: number; hashFields; @@ -57,43 +55,6 @@ export class ConsentHandler { this.#defer.resolve(data); } - /** - * Set CMP API reference - * @param cmpApi - CMP API reference - */ - setCmpApi(cmpApi: any) { - this.#cmpApi = cmpApi; - } - - /** - * Get CMP API reference - */ - getCmpApi() { - return this.#cmpApi; - } - - /** - * Set CMP listener ID - * @param listenerId - Unique identifier for the CMP listener - */ - setCmpListenerId(listenerId: number | undefined) { - this.#listenerId = listenerId; - } - - /** - * Get CMP listener ID - */ - getCmpListenerId() { - return this.#listenerId; - } - - resetCmpApis(success: boolean) { - if (success) { - this.#cmpApi = null; - this.#listenerId = undefined; - } - } - /** * reset this handler (mainly for tests) */ @@ -164,24 +125,6 @@ export class ConsentHandler { } return this.#hash; } - - addApiVersionToParams(params: any) {} - - // Base class defines the algorithm structure - removeCmpEventListener() { - if (this.getCmpApi() && this.getCmpListenerId() !== undefined && this.getCmpListenerId() !== null) { - const params = { - command: "removeEventListener", - callback: this.resetCmpApis.bind(this), - parameter: this.getCmpListenerId(), - }; - - // Call the method that subclasses will override - this.addApiVersionToParams(params); - - this.getCmpApi()(params); - } - } } class UspConsentHandler extends ConsentHandler> { @@ -200,10 +143,6 @@ class GdprConsentHandler extends ConsentHandler { }); describe('configParser', () => { - let namespace, displayName, consentDataHandler, parseConsentData, getNullConsent, cmpHandlers; + let namespace, displayName, consentDataHandler, parseConsentData, getNullConsent, cmpHandlers, cmpEventCleanup; let getConsentConfig, resetConsentDataHandler; beforeEach(() => { @@ -246,6 +246,7 @@ describe('consent management utils', () => { cmpHandlers = { iab: sinon.stub().returns(Promise.resolve()) }; + cmpEventCleanup = sinon.stub(); // Create a spy for resetConsentDataHandler to verify it's called const configParserInstance = configParser({ @@ -254,7 +255,8 @@ describe('consent management utils', () => { consentDataHandler, parseConsentData, getNullConsent, - cmpHandlers + cmpHandlers, + cmpEventCleanup }); getConsentConfig = configParserInstance; @@ -278,22 +280,69 @@ describe('consent management utils', () => { expect(result).to.deep.equal({}); sinon.assert.calledWith(utils.logWarn, sinon.match('config enabled is set to false')); - sinon.assert.called(consentDataHandler.removeCmpEventListener); - sinon.assert.called(consentDataHandler.reset); }); - it('should not reset consent data handler when enabled is true', () => { + it('should call cmpEventCleanup when enabled is false', () => { + getConsentConfig({[namespace]: {enabled: false}}); + + sinon.assert.called(cmpEventCleanup); + sinon.assert.calledWith(utils.logWarn, sinon.match('config enabled is set to false')); + }); + + it('should handle cmpEventCleanup errors gracefully', () => { + const cleanupError = new Error('Cleanup failed'); + cmpEventCleanup.throws(cleanupError); + + getConsentConfig({[namespace]: {enabled: false}}); + + sinon.assert.called(cmpEventCleanup); + sinon.assert.calledWith(utils.logError, sinon.match('Error during CMP event cleanup'), cleanupError); + }); + + it('should not call cmpEventCleanup when enabled is true', () => { getConsentConfig({[namespace]: {enabled: true, cmpApi: 'iab'}}); - sinon.assert.notCalled(consentDataHandler.removeCmpEventListener); - sinon.assert.notCalled(consentDataHandler.reset); + sinon.assert.notCalled(cmpEventCleanup); }); - it('should not reset consent data handler when enabled is not specified', () => { + it('should not call cmpEventCleanup when enabled is not specified', () => { getConsentConfig({[namespace]: {cmpApi: 'iab'}}); - sinon.assert.notCalled(consentDataHandler.removeCmpEventListener); - sinon.assert.notCalled(consentDataHandler.reset); + sinon.assert.notCalled(cmpEventCleanup); + }); + }); + + describe('cmpEventCleanup parameter', () => { + it('should work without cmpEventCleanup parameter', () => { + const configParserWithoutCleanup = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers + // No cmpEventCleanup provided + }); + + const result = configParserWithoutCleanup({[namespace]: {enabled: false}}); + expect(result).to.deep.equal({}); + // Should not throw error when cmpEventCleanup is undefined + }); + + it('should only call cmpEventCleanup if it is a function', () => { + const configParserWithNonFunction = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers, + cmpEventCleanup: 'not a function' + }); + + const result = configParserWithNonFunction({[namespace]: {enabled: false}}); + expect(result).to.deep.equal({}); + // Should not throw error when cmpEventCleanup is not a function }); }); diff --git a/test/spec/libraries/cmp/cmpEventUtils_spec.js b/test/spec/libraries/cmp/cmpEventUtils_spec.js new file mode 100644 index 00000000000..c3262549b20 --- /dev/null +++ b/test/spec/libraries/cmp/cmpEventUtils_spec.js @@ -0,0 +1,330 @@ +import * as utils from 'src/utils.js'; +import { + BaseCmpEventManager, + TcfCmpEventManager, + GppCmpEventManager, + createCmpEventManager +} from '../../../../libraries/cmp/cmpEventUtils.js'; + +describe('CMP Event Utils', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + ['logError', 'logInfo', 'logWarn'].forEach(n => sandbox.stub(utils, n)); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('BaseCmpEventManager', () => { + let manager; + + // Create a concrete implementation for testing the abstract class + class TestCmpEventManager extends BaseCmpEventManager { + removeCmpEventListener() { + const params = this.getRemoveListenerParams(); + if (params) { + this.getCmpApi()(params); + } + } + } + + beforeEach(() => { + manager = new TestCmpEventManager(); + }); + + describe('setCmpApi and getCmpApi', () => { + it('should set and get CMP API', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + expect(manager.getCmpApi()).to.equal(mockCmpApi); + }); + + it('should initialize with null CMP API', () => { + expect(manager.getCmpApi()).to.be.null; + }); + }); + + describe('setCmpListenerId and getCmpListenerId', () => { + it('should set and get listener ID', () => { + manager.setCmpListenerId(123); + expect(manager.getCmpListenerId()).to.equal(123); + }); + + it('should handle undefined listener ID', () => { + manager.setCmpListenerId(undefined); + expect(manager.getCmpListenerId()).to.be.undefined; + }); + + it('should handle zero as valid listener ID', () => { + manager.setCmpListenerId(0); + expect(manager.getCmpListenerId()).to.equal(0); + }); + + it('should initialize with undefined listener ID', () => { + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('resetCmpApis', () => { + it('should reset both CMP API and listener ID', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(456); + + manager.resetCmpApis(); + + expect(manager.getCmpApi()).to.be.null; + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('getRemoveListenerParams', () => { + it('should return params when CMP API and listener ID are valid', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + + expect(params).to.not.be.null; + expect(params.command).to.equal('removeEventListener'); + expect(params.parameter).to.equal(123); + expect(params.callback).to.be.a('function'); + }); + + it('should return null when CMP API is null', () => { + manager.setCmpApi(null); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when CMP API is not a function', () => { + manager.setCmpApi('not a function'); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when listener ID is undefined', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(undefined); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when listener ID is null', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(null); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return params when listener ID is 0', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(0); + + const params = manager.getRemoveListenerParams(); + expect(params).to.not.be.null; + expect(params.parameter).to.equal(0); + }); + + it('should call resetCmpApis when callback is executed', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + params.callback(); + + expect(manager.getCmpApi()).to.be.null; + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with params when conditions are met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(123); + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + }); + }); + }); + + describe('TcfCmpEventManager', () => { + let manager, mockGetConsentData; + + beforeEach(() => { + mockGetConsentData = sinon.stub(); + manager = new TcfCmpEventManager(mockGetConsentData); + }); + + it('should initialize with provided getConsentData function', () => { + expect(manager.getConsentData).to.equal(mockGetConsentData); + }); + + it('should initialize with default getConsentData when not provided', () => { + const defaultManager = new TcfCmpEventManager(); + expect(defaultManager.getConsentData()).to.be.null; + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with TCF-specific params including apiVersion', () => { + const mockCmpApi = sinon.stub(); + const consentData = { apiVersion: 2 }; + mockGetConsentData.returns(consentData); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(456); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + sinon.assert.calledWith(utils.logInfo, 'Removing TCF CMP event listener'); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(456); + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should use default apiVersion when consent data has no apiVersion', () => { + const mockCmpApi = sinon.stub(); + const consentData = {}; // No apiVersion + mockGetConsentData.returns(consentData); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(789); + + manager.removeCmpEventListener(); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should use default apiVersion when consent data is null', () => { + const mockCmpApi = sinon.stub(); + mockGetConsentData.returns(null); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(789); + + manager.removeCmpEventListener(); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + sinon.assert.notCalled(utils.logInfo); + }); + }); + }); + + describe('GppCmpEventManager', () => { + let manager; + + beforeEach(() => { + manager = new GppCmpEventManager(); + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with GPP-specific params', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(321); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + sinon.assert.calledWith(utils.logInfo, 'Removing GPP CMP event listener'); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(321); + expect(callArgs.apiVersion).to.be.undefined; // GPP doesn't set apiVersion + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + sinon.assert.notCalled(utils.logInfo); + }); + }); + }); + + describe('createCmpEventManager', () => { + it('should create TcfCmpEventManager for tcf type', () => { + const mockGetConsentData = sinon.stub(); + const manager = createCmpEventManager('tcf', mockGetConsentData); + + expect(manager).to.be.instanceOf(TcfCmpEventManager); + expect(manager.getConsentData).to.equal(mockGetConsentData); + }); + + it('should create TcfCmpEventManager without getConsentData function', () => { + const manager = createCmpEventManager('tcf'); + + expect(manager).to.be.instanceOf(TcfCmpEventManager); + expect(manager.getConsentData()).to.be.null; + }); + + it('should create GppCmpEventManager for gpp type', () => { + const manager = createCmpEventManager('gpp'); + + expect(manager).to.be.instanceOf(GppCmpEventManager); + }); + + it('should log error and return null for unknown type', () => { + const manager = createCmpEventManager('unknown'); + + expect(manager).to.be.null; + sinon.assert.calledWith(utils.logError, 'Unknown CMP type: unknown'); + }); + + it('should ignore getConsentData parameter for gpp type', () => { + const mockGetConsentData = sinon.stub(); + const manager = createCmpEventManager('gpp', mockGetConsentData); + + expect(manager).to.be.instanceOf(GppCmpEventManager); + // GPP manager doesn't use getConsentData + }); + }); +}); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 0c3aad51aad..dfe5f3d6a0e 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,4 @@ -import {consentConfig, gdprScope, resetConsentData, setConsentConfig, } from 'modules/consentManagementTcf.js'; +import {consentConfig, gdprScope, resetConsentData, setConsentConfig, tcfCmpEventManager} from 'modules/consentManagementTcf.js'; import {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; @@ -746,7 +746,8 @@ describe('consentManagement', function () { listenerId: 123 }; - const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { args[2](testConsentData, true); @@ -770,7 +771,8 @@ describe('consentManagement', function () { listenerId: null }; - const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { args[2](testConsentData, true); @@ -793,7 +795,8 @@ describe('consentManagement', function () { listenerId: undefined }; - const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { args[2](testConsentData, true); @@ -803,7 +806,6 @@ describe('consentManagement', function () { expect(await runHook()).to.be.true; sinon.assert.notCalled(setCmpListenerIdSpy); - setCmpListenerIdSpy.restore(); }); @@ -816,7 +818,8 @@ describe('consentManagement', function () { listenerId: 0 }; - const setCmpListenerIdSpy = sinon.spy(gdprDataHandler, 'setCmpListenerId'); + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { args[2](testConsentData, true); @@ -827,7 +830,6 @@ describe('consentManagement', function () { sinon.assert.calledOnce(setCmpListenerIdSpy); sinon.assert.calledWith(setCmpListenerIdSpy, 0); - setCmpListenerIdSpy.restore(); }); @@ -839,7 +841,8 @@ describe('consentManagement', function () { eventStatus: 'tcloaded' }; - const setCmpApiSpy = sinon.spy(gdprDataHandler, 'setCmpApi'); + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpApiSpy = sinon.spy(tcfCmpEventManager, 'setCmpApi'); cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { args[2](testConsentData, true); @@ -850,7 +853,6 @@ describe('consentManagement', function () { sinon.assert.calledOnce(setCmpApiSpy); expect(setCmpApiSpy.getCall(0).args[0]).to.be.a('function'); - setCmpApiSpy.restore(); }); }); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 26635677933..1bcad3216ce 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -85,112 +85,6 @@ describe('Consent data handler', () => { expect(handler.hash).to.eql(h1); }) }) - - describe('removeCmpEventListener', () => { - let cmpApiStub; - - beforeEach(() => { - cmpApiStub = sinon.stub(); - handler.setCmpApi(cmpApiStub); - }); - - it('should not call CMP API when no listener ID is set', () => { - handler.setCmpListenerId(undefined); - handler.removeCmpEventListener(); - sinon.assert.notCalled(cmpApiStub); - }); - - it('should not call CMP API when listener ID is null', () => { - handler.setCmpListenerId(null); - handler.removeCmpEventListener(); - sinon.assert.notCalled(cmpApiStub); - }); - - it('should not call CMP API when CMP API is not set', () => { - handler.setCmpApi(null); - handler.setCmpListenerId(123); - handler.removeCmpEventListener(); - sinon.assert.notCalled(cmpApiStub); - }); - - it('should call CMP API with correct parameters when both API and listener ID are set', () => { - const listenerId = 123; - handler.setCmpListenerId(listenerId); - - // Create spy for addApiVersionToParams - const addApiVersionToParamsSpy = sinon.spy(handler, 'addApiVersionToParams'); - - handler.removeCmpEventListener(); - - // Verify addApiVersionToParams was called with correct parameters - sinon.assert.calledOnce(addApiVersionToParamsSpy); - const params = addApiVersionToParamsSpy.firstCall.args[0]; - expect(params).to.have.property('command', 'removeEventListener'); - expect(params).to.have.property('parameter', listenerId); - expect(params.callback).to.be.a('function'); - - // Verify CMP API was called with the same parameters - sinon.assert.calledOnce(cmpApiStub); - sinon.assert.calledWith(cmpApiStub, params); - - // Clean up spy - addApiVersionToParamsSpy.restore(); - }); - - it('should reset CMP APIs when callback is called with success=true', () => { - const listenerId = 123; - handler.setCmpListenerId(listenerId); - - handler.removeCmpEventListener(); - - // Get the callback that was passed to the CMP API - const callback = cmpApiStub.firstCall.args[0].callback; - - // Call the callback with success=true - callback({}, true); - - // Verify CMP API and listener ID are reset - expect(handler.getCmpApi()).to.be.null; - expect(handler.getCmpListenerId()).to.be.undefined; - }); - - it('should not reset CMP APIs when callback is called with success=false', () => { - const listenerId = 123; - handler.setCmpListenerId(listenerId); - - handler.removeCmpEventListener(); - - // Get the callback that was passed to the CMP API - const callback = cmpApiStub.firstCall.args[0].callback; - - // Call the callback with success=false - callback(null, false); - - // Verify CMP API and listener ID are not reset - expect(handler.getCmpApi()).to.equal(cmpApiStub); - expect(handler.getCmpListenerId()).to.equal(listenerId); - }); - - it('should call addApiVersionToParams to allow subclasses to customize parameters', () => { - // Create a subclass that overrides addApiVersionToParams - class TestConsentHandler extends ConsentHandler { - addApiVersionToParams(params) { - params.version = 2; - } - } - - const testHandler = new TestConsentHandler(); - const testCmpApiStub = sinon.stub(); - testHandler.setCmpApi(testCmpApiStub); - testHandler.setCmpListenerId(456); - - testHandler.removeCmpEventListener(); - - // Verify CMP API was called with the customized parameters - sinon.assert.calledOnce(testCmpApiStub); - expect(testCmpApiStub.firstCall.args[0]).to.have.property('version', 2); - }); - }); }); describe('multiHandler', () => { From a5430ef901eed30bfb1a3be99a26213f88a1f4bb Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Mon, 27 Oct 2025 12:11:30 +0530 Subject: [PATCH 10/11] Remove stray comment --- src/consentHandler.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/consentHandler.ts b/src/consentHandler.ts index 2691429e241..b3fa9594673 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -140,9 +140,6 @@ class UspConsentHandler extends ConsentHandler> { hashFields = ["gdprApplies", "consentString"]; - /** - * Remove CMP event listener using CMP API - */ getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { From 79d66dbcc53766f1ef09649274de84b85c6713c6 Mon Sep 17 00:00:00 2001 From: Nitin Shirsat Date: Mon, 27 Oct 2025 14:00:07 +0530 Subject: [PATCH 11/11] Fix test cases issues --- test/spec/modules/consentManagementGpp_spec.js | 1 + test/spec/unit/core/adapterManager_spec.js | 1 + 2 files changed, 2 insertions(+) diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 6adf159ff55..e143311a880 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -211,6 +211,7 @@ describe('consentManagementGpp', function () { } }; gppClient = mockClient(); + gppDataHandler.enable(); }); describe('updateConsent', () => { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index c8d49c15548..0be07704ed2 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -2615,6 +2615,7 @@ describe('adapterManager tests', function () { describe('gdpr consent module', function () { it('inserts gdprConsent object to bidRequest only when module was enabled', function () { + gdprDataHandler.enable(); gdprDataHandler.setConsentData({ consentString: 'abc123def456', consentRequired: true