Skip to content

Commit

Permalink
restructured 1.1 CMP iframe code, renamed utils function, cleaned up …
Browse files Browse the repository at this point in the history
…unit tests
  • Loading branch information
jsnellbaker committed Apr 26, 2018
1 parent effa5b5 commit a3ca63f
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 41 deletions.
67 changes: 48 additions & 19 deletions modules/consentManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,30 @@ const cmpCallMap = {
};

/**
* This function handles interacting with the AppNexus CMP to obtain the consentObject value of the user.
* This function handles interacting with an IAB compliant CMP to obtain the consentObject value of the user.
* Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
* based on the appropriate result.
* @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
* @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
*/
function lookupIabConsent(cmpSuccess, cmpError) {
// let callId = 0;
let cmpCallbacks;

if (window.__cmp) {
// check if the CMP is located on the same window level as the prebid code.
// if it's found, directly call the CMP via it's API and call the cmpSuccess callback.
// if it's not found, assume the prebid code may be inside an iframe and the CMP code is located in a higher parent window.
// in this case, use the IAB's iframe locator sample code (which is slightly cutomized) to try to find the CMP and use postMessage() to communicate with the CMP.
if (utils.isFn(window.__cmp)) {
window.__cmp('getVendorConsents', null, cmpSuccess);
} else {
/** START OF STOCK CODE FROM IAB 1.1 CMP SEC */
callCmpWhileInIframe();
}

function callCmpWhileInIframe() {
/**
* START OF STOCK CODE FROM IAB 1.1 CMP SPEC
*/

// find the CMP frame
let f = window;
let cmpFrame;
Expand All @@ -56,12 +67,14 @@ function lookupIabConsent(cmpSuccess, cmpError) {
f = f.parent;
}

let cmpCallbacks = {};
cmpCallbacks = {};

/* Setup up a __cmp function to do the postMessage and stash the callback.
This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
window.__cmp = function(cmd, arg, callback) {
if (!cmpFrame) {
removePostMessageListener();

let errmsg = 'CMP not found';
// small customization to properly return error
return cmpError(errmsg);
Expand All @@ -77,18 +90,34 @@ function lookupIabConsent(cmpSuccess, cmpError) {
}

/** when we get the return message, call the stashed callback */
window.addEventListener('message', function(event) {
// small customization to prevent reading strings from other sources that aren't JSON.stringified
let json = (typeof event.data === 'string' && includes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
if (json.__cmpReturn) {
let i = json.__cmpReturn;
cmpCallbacks[i.callId](i.returnValue, i.success);
delete cmpCallbacks[i.callId];
}
}, false);
// small customization to remove this eventListener later in module
window.addEventListener('message', readPostMessageResponse, false);

/** END OF STOCK CODE FROM IAB 1.1 CMP SEC */
window.__cmp('getVendorConsents', null, cmpSuccess);
/**
* END OF STOCK CODE FROM IAB 1.1 CMP SPEC
*/

// call CMP
window.__cmp('getVendorConsents', null, cmpIframeCallback);
}

function readPostMessageResponse(event) {
// small customization to prevent reading strings from other sources that aren't JSON.stringified
let json = (typeof event.data === 'string' && includes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
if (json.__cmpReturn) {
let i = json.__cmpReturn;
cmpCallbacks[i.callId](i.returnValue, i.success);
delete cmpCallbacks[i.callId];
}
}

function removePostMessageListener() {
window.removeEventListener('message', readPostMessageResponse, false);
}

function cmpIframeCallback(consentObject) {
removePostMessageListener();
cmpSuccess(consentObject);
}
}

Expand Down Expand Up @@ -135,7 +164,7 @@ export function requestBidsHook(config, fn) {
* @param {object} consentObject required; object returned by CMP that contains user's consent choices
*/
function processCmpData(consentObject) {
if (!utils.isObject(consentObject) || typeof consentObject.metadata !== 'string' || consentObject.metadata === '') {
if (!utils.isPlainObject(consentObject) || !utils.isStr(consentObject.metadata) || consentObject.metadata === '') {
cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`);
} else {
clearTimeout(timer);
Expand Down Expand Up @@ -223,14 +252,14 @@ export function resetConsentData() {
* @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean)
*/
export function setConfig(config) {
if (typeof config.cmpApi === 'string') {
if (utils.isStr(config.cmpApi)) {
userCMP = config.cmpApi;
} else {
userCMP = DEFAULT_CMP;
utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`);
}

if (typeof config.timeout === 'number') {
if (utils.isNumber(config.timeout)) {
consentTimeout = config.timeout;
} else {
consentTimeout = DEFAULT_CONSENT_TIMEOUT;
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ exports.isNumber = function(object) {
return this.isA(object, t_Numb);
};

exports.isObject = function(object) {
exports.isPlainObject = function(object) {
return this.isA(object, t_Object);
}

Expand Down
31 changes: 16 additions & 15 deletions test/spec/modules/consentManagement_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ describe('consentManagement', function () {
describe('requestBidsHook tests:', () => {
let goodConfigWithCancelAuction = {
cmpApi: 'iab',
timeout: 750,
timeout: 7500,
allowAuctionWithoutConsent: false
};

let goodConfigWithAllowAuction = {
cmpApi: 'iab',
timeout: 750,
timeout: 7500,
allowAuctionWithoutConsent: true
};

Expand Down Expand Up @@ -117,21 +117,21 @@ describe('consentManagement', function () {
});

it('should bypass CMP and simply use previously stored consentData', () => {
let testConsentString = {
let testConsentData = {
gdprApplies: true,
metadata: 'xyz'
};

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentString);
args[2](testConsentData);
});
setConfig(goodConfigWithAllowAuction);
requestBidsHook({}, () => {});
cmpStub.restore();

// reset the stub to ensure it wasn't called during the second round of calls
cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentString);
args[2](testConsentData);
});

requestBidsHook({}, () => {
Expand All @@ -140,7 +140,7 @@ describe('consentManagement', function () {
let consent = gdprDataHandler.getConsentData();

expect(didHookReturn).to.be.true;
expect(consent.consentString).to.equal(testConsentString.metadata);
expect(consent.consentString).to.equal(testConsentData.metadata);
expect(consent.gdprApplies).to.be.true;
sinon.assert.notCalled(cmpStub);
});
Expand All @@ -163,13 +163,14 @@ describe('consentManagement', function () {
$$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
eventStub.restore();
cmpStub.restore();
delete window.__cmp;
utils.logError.restore();
utils.logWarn.restore();
gdprDataHandler.consentData = null;
});

it('should return the consent string from a postmessage + addEventListener response', () => {
let testConsentString = {
let testConsentData = {
data: {
__cmpReturn: {
returnValue: {
Expand All @@ -180,7 +181,7 @@ describe('consentManagement', function () {
}
};
eventStub = sinon.stub(window, 'addEventListener').callsFake((...args) => {
args[1](testConsentString);
args[1](testConsentData);
});
cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2]({
Expand Down Expand Up @@ -226,12 +227,12 @@ describe('consentManagement', function () {
});

it('performs lookup check and stores consentData for a valid existing user', () => {
let testConsentString = {
let testConsentData = {
gdprApplies: true,
metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
};
cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentString);
args[2](testConsentData);
});

setConfig(goodConfigWithAllowAuction);
Expand All @@ -244,15 +245,15 @@ describe('consentManagement', function () {
sinon.assert.notCalled(utils.logWarn);
sinon.assert.notCalled(utils.logError);
expect(didHookReturn).to.be.true;
expect(consent.consentString).to.equal(testConsentString.metadata);
expect(consent.consentString).to.equal(testConsentData.metadata);
expect(consent.gdprApplies).to.be.true;
});

it('throws an error when processCmpData check failed while config had allowAuction set to false', () => {
let testConsentString = null;
let testConsentData = null;

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentString);
args[2](testConsentData);
});

setConfig(goodConfigWithCancelAuction);
Expand All @@ -268,10 +269,10 @@ describe('consentManagement', function () {
});

it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', () => {
let testConsentString = null;
let testConsentData = null;

cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
args[2](testConsentString);
args[2](testConsentData);
});

setConfig(goodConfigWithAllowAuction);
Expand Down
12 changes: 6 additions & 6 deletions test/spec/utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,29 +359,29 @@ describe('Utils', function () {
});
});

describe('isObject', function () {
describe('isPlainObject', function () {
it('should return false with input string', function () {
var output = utils.isObject(obj_string);
var output = utils.isPlainObject(obj_string);
assert.deepEqual(output, false);
});

it('should return false with input number', function () {
var output = utils.isObject(obj_number);
var output = utils.isPlainObject(obj_number);
assert.deepEqual(output, false);
});

it('should return true with input object', function () {
var output = utils.isObject(obj_object);
var output = utils.isPlainObject(obj_object);
assert.deepEqual(output, true);
});

it('should return false with input array', function () {
var output = utils.isObject(obj_array);
var output = utils.isPlainObject(obj_array);
assert.deepEqual(output, false);
});

it('should return false with input function', function () {
var output = utils.isObject(obj_function);
var output = utils.isPlainObject(obj_function);
assert.deepEqual(output, false);
});
});
Expand Down

0 comments on commit a3ca63f

Please sign in to comment.