diff --git a/gulpfile.js b/gulpfile.js index ff49436384b0..3a2740c944c5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -393,7 +393,7 @@ gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, addBanner, gulpBundle // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); -gulp.task('test', gulp.series(clean, lint, 'test-only')); +gulp.task('test', gulp.series(clean, 'test-only')); gulp.task('test-coverage', gulp.series(clean, testCoverage)); gulp.task(viewCoverage); diff --git a/modules/yieldmoSyntheticInventoryModule.js b/modules/yieldmoSyntheticInventoryModule.js index cda90c2f5780..2d2b0a3ee33f 100644 --- a/modules/yieldmoSyntheticInventoryModule.js +++ b/modules/yieldmoSyntheticInventoryModule.js @@ -1,53 +1,74 @@ import { config } from '../src/config.js'; -import { isGptPubadsDefined, isFn } from '../src/utils.js'; +import { isGptPubadsDefined } from '../src/utils.js'; import * as ajax from '../src/ajax.js' -import strIncludes from 'core-js-pure/features/string/includes.js'; - -export const MODULE_NAME = 'Yieldmo Synthetic Inventory Module'; -export const AD_SERVER_ENDPOINT = 'https://ads.yieldmo.com/v002/t_ads/ads'; -const USPAPI_VERSION = 1; +import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; + +const MODULE_NAME = 'yieldmoSyntheticInventory'; +const AD_SERVER_ENDPOINT = 'https://ads.yieldmo.com/v002/t_ads/ads'; +const GET_CONFIG_TIMEOUT = 10; // might be 0, 10 just in case + +export const testExports = { + MODULE_NAME, + validateConfig, + setGoogleTag, + setAd, + getConsentData, + getConfigs, + processAdResponse, + getAd +}; + +function getConsentData() { + return new Promise((resolve) => { + Promise.allSettled([ + gdprDataHandler.promise, + uspDataHandler.promise + ]) + .then(([ cmp, usp ]) => { + resolve({ + cmp: cmp.value, + usp: usp.value + }); + }) + }); +} -let cmpVersion = 0; -let cmpResolved = false; +function setGoogleTag() { + if (!isGptPubadsDefined()) { + window.top.googletag = window.top.googletag || {}; + window.top.googletag.cmd = window.top.googletag.cmd || []; + } +} -export function init(config) { - checkSandbox(window); - validateConfig(config); +function setAd(config, ad) { + window.top.__ymAds = processAdResponse(ad); + const googletag = window.top.googletag; + googletag.cmd.push(() => { + if (window.top.document.body) { + googletagCmd(config, googletag); + } else { + window.top.document.addEventListener('DOMContentLoaded', () => googletagCmd(config, googletag)); + } + }); +} - const consentData = () => { - const consentDataObj = {}; - return (api, result) => { - consentDataObj[api] = result; - if ('cmp' in consentDataObj && 'usp' in consentDataObj) { - if (!isGptPubadsDefined()) { - window.top.googletag = window.top.googletag || {}; - window.top.googletag.cmd = window.top.googletag.cmd || []; - } - ajax.ajaxBuilder()(`${AD_SERVER_ENDPOINT}?${serialize(collectData(config.placementId, consentDataObj))}`, { - success: (responceText, responseObj) => { - window.top.__ymAds = processResponse(responseObj); - const googletag = window.top.googletag; - googletag.cmd.push(() => { - if (window.top.document.body) { - googletagCmd(config, googletag); - } else { - window.top.document.addEventListener('DOMContentLoaded', () => googletagCmd(config, googletag)); - } - }); - }, - error: (message, err) => { - throw err; - } - }); +function getAd(config, consentData) { + const url = `${AD_SERVER_ENDPOINT}?${serialize(collectData(config.placementId, consentData))}`; + return new Promise((resolve, reject) => + ajax.ajaxBuilder()(url, { + success: (responseText, responseObj) => { + resolve(responseObj); + }, + error: (message, err) => { + reject(new Error(`${MODULE_NAME}: ad server error: ${err.status}`)); } - } - }; - const consentDataHandler = consentData(); - lookupIabConsent((a) => consentDataHandler('cmp', a), (e) => consentDataHandler('cmp', false)); - lookupUspConsent((a) => consentDataHandler('usp', a), (e) => consentDataHandler('usp', false)); + })) + .catch(err => { + throw err; + }); } -export function validateConfig(config) { +function validateConfig(config) { if (!('placementId' in config)) { throw new Error(`${MODULE_NAME}: placementId required`); } @@ -93,8 +114,8 @@ function collectData(placementId, consentDataObj) { bwe: typeof connection.downlink !== 'undefined' ? connection.downlink + 'Mb/sec' : undefined, rtt: typeof connection.rtt !== 'undefined' ? String(connection.rtt) : undefined, sd: typeof connection.saveData !== 'undefined' ? String(connection.saveData) : undefined, - us_privacy: (consentDataObj.usp && consentDataObj.usp.usPrivacy) || '', - cmp: (consentDataObj.cmp && consentDataObj.cmp.tcString) || '' + us_privacy: consentDataObj.usp || '', + cmp: (consentDataObj.cmp && consentDataObj.cmp.consentString) || '' }; } @@ -108,15 +129,19 @@ function serialize(dataObj) { return str.join('&'); } -function processResponse(res) { +function processAdResponse(res) { + if (res.status >= 300) { + throw new Error(`${MODULE_NAME}: ad server error: ${res.status}`); + // 204 is a valid response, but we're throwing because it's always good to know + // probably something has been wrong configured (placementId / adUnitPath / userConsent ...) + } else if (res.status === 204) { + throw new Error(`${MODULE_NAME}: ${res.status} - no ad to serve`); + } let parsedResponseBody; try { parsedResponseBody = JSON.parse(res.responseText); } catch (err) { - throw new Error(`${MODULE_NAME}: response is not valid JSON`); - } - if (res && res.status === 204) { - throw new Error(`${MODULE_NAME}: no content success status`); + throw new Error(`${MODULE_NAME}: JSON validation error`); } if (parsedResponseBody.data && parsedResponseBody.data.length && parsedResponseBody.data[0].error_code) { throw new Error(`${MODULE_NAME}: no ad, error_code: ${parsedResponseBody.data[0].error_code}`); @@ -131,194 +156,47 @@ function checkSandbox(w) { throw new Error(`${MODULE_NAME}: module was placed in the sandbox iframe`); } } - -function lookupIabConsent(cmpSuccess, cmpError) { - function findCMP() { - let f = window; - let cmpFrame; - let cmpFunction; - - while (!cmpFrame) { - try { - if (isFn(f.__tcfapi)) { - cmpVersion = 2; - cmpFunction = f.__tcfapi; - cmpFrame = f; - continue; - } - } catch (e) { } - - try { - if (f.frames['__tcfapiLocator']) { - cmpVersion = 2; - cmpFrame = f; - continue; - } - } catch (e) { } - - if (f === window.top) break; - f = f.parent; - } - return { - cmpFrame, - cmpFunction - }; - } - - function cmpResponseCallback(tcfData, success) { - if (success) { - setTimeout(() => { - if (!cmpResolved) { - cmpSuccess(tcfData); - } - }, 3000); - if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { - cmpSuccess(tcfData); - cmpResolved = true; - } - } else { - cmpError('CMP unable to register callback function. Please check CMP setup.'); - } - } - - let { cmpFrame, cmpFunction } = findCMP(); - - if (!cmpFrame) { - return cmpError('CMP not found.'); - } - - if (isFn(cmpFunction)) { - cmpFunction('addEventListener', cmpVersion, cmpResponseCallback); - } else { - callCmpWhileInIframe('addEventListener', cmpFrame, cmpResponseCallback); - } - - function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { - let apiName = '__tcfapi'; - let callName = `${apiName}Call`; - let callId = Math.random() + ''; - let msg = { - [callName]: { - command: commandName, - version: cmpVersion, - parameter: undefined, - callId: callId - } - }; - - cmpFrame.postMessage(msg, '*'); - - window.addEventListener('message', readPostMessageResponse, false); - - function readPostMessageResponse(event) { - let cmpDataPkgName = `${apiName}Return`; - let json = (typeof event.data === 'string' && strIncludes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; - if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - let payload = json[cmpDataPkgName]; - - if (payload.callId === callId) { - moduleCallback(payload.returnValue, payload.success); - } - } - } - } -} - -function lookupUspConsent(uspSuccess, uspError) { - function findUsp() { - let f = window; - let uspapiFrame; - let uspapiFunction; - - while (!uspapiFrame) { - try { - if (isFn(f.__uspapi)) { - uspapiFunction = f.__uspapi; - uspapiFrame = f; - continue; - } - } catch (e) {} - - try { - if (f.frames['__uspapiLocator']) { - uspapiFrame = f; - continue; - } - } catch (e) {} - if (f === window.top) break; - f = f.parent; - } - return { - uspapiFrame, - uspapiFunction, - }; - } - - function handleUspApiResponseCallbacks() { - const uspResponse = {}; - - function afterEach() { - if (uspResponse.usPrivacy) { - uspSuccess(uspResponse); - } else { - uspError('Unable to get USP consent string.'); - } - } - - return { - consentDataCallback: (consentResponse, success) => { - if (success && consentResponse.uspString) { - uspResponse.usPrivacy = consentResponse.uspString; - } - afterEach(); - }, - }; - } - - let callbackHandler = handleUspApiResponseCallbacks(); - let { uspapiFrame, uspapiFunction } = findUsp(); - - if (!uspapiFrame) { - return uspError('USP CMP not found.'); - } - - if (isFn(uspapiFunction)) { - uspapiFunction( - 'getUSPData', - USPAPI_VERSION, - callbackHandler.consentDataCallback - ); - } else { - callUspApiWhileInIframe( - 'getUSPData', - uspapiFrame, - callbackHandler.consentDataCallback - ); - } - - function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { - let callId = Math.random() + ''; - let msg = { - __uspapiCall: { - command: commandName, - version: USPAPI_VERSION, - callId: callId, - }, - }; - - uspapiFrame.postMessage(msg, '*'); - - window.addEventListener('message', readPostMessageResponse, false); - - function readPostMessageResponse(event) { - const res = event && event.data && event.data.__uspapiReturn; - if (res && res.callId) { - if (res.callId === callId) { - moduleCallback(res.returnValue, res.success); - } - } - } - } +/** + * Configs will be available only next JS event loop iteration after calling config.getConfig, + * but... if user won't provide the configs, callback will never be executed + * because of that we're using promises for the code readability (to prevent callback hell), + * and setTimeout(__, 0) as a fallback in case configs wasn't provided... +*/ +function getConfigs() { + const promisifyGetConfig = configName => + new Promise((resolve) => + config.getConfig(configName, config => resolve(config))); + + const getConfigPromise = (moduleName) => { + let timer; + // Promise has a higher priority than callback, so it should be there first + return Promise.race([ + promisifyGetConfig(moduleName), + // will be rejected if config wasn't provided in GET_CONFIG_TIMEOUT ms + new Promise((resolve, reject) => timer = setTimeout(reject, + GET_CONFIG_TIMEOUT, + new Error(`${MODULE_NAME}: ${moduleName} was not configured`))) + ]).finally(() => + clearTimeout(timer)); + }; + // We're expecting to get both yieldmoSyntheticInventory + // and consentManagement configs, so if one of them configs will be rejected -- + // getConfigs will be rejected as well + return Promise.all([ + getConfigPromise('yieldmoSyntheticInventory'), + getConfigPromise('consentManagement'), + ]) } -config.getConfig('yieldmo_synthetic_inventory', config => init(config.yieldmo_synthetic_inventory)); +getConfigs() + .then(configs => { + const siConfig = configs[0].yieldmoSyntheticInventory; + validateConfig(siConfig); + checkSandbox(window); + setGoogleTag(); + getConsentData() + .then(consentData => + getAd(siConfig, consentData)) + .then(ad => + setAd(siConfig, ad)) + }) diff --git a/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js b/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js index e04f9f013888..8e3a9016ed8e 100644 --- a/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js +++ b/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js @@ -1,10 +1,7 @@ import { expect } from 'chai'; import * as ajax from 'src/ajax.js'; -import { - init, - MODULE_NAME, - validateConfig -} from 'modules/yieldmoSyntheticInventoryModule'; +import { testExports } from 'modules/yieldmoSyntheticInventoryModule'; +import { config } from 'src/config.js'; const mockedYmConfig = { placementId: '123456', @@ -41,43 +38,69 @@ const getQuearyParamsFromUrl = (url) => ); describe('Yieldmo Synthetic Inventory Module', function() { - let config = Object.assign({}, mockedYmConfig); let googletagBkp; + let sandbox; beforeEach(function () { googletagBkp = window.googletag; delete window.googletag; + sandbox = sinon.sandbox.create(); }); afterEach(function () { window.googletag = googletagBkp; + sandbox.restore(); }); - it('should throw an error if placementId is missed', function() { - const {placementId, ...config} = mockedYmConfig; + describe('Module config initialization', () => { + it('getConfigs should call config.getConfig twice to get yieldmoSyntheticInventory and consentManagement configs', function() { + const getConfigStub = sandbox.stub(config, 'getConfig').returns({}); - expect(function () { - validateConfig(config); - }).throw(`${MODULE_NAME}: placementId required`); + return testExports.getConfigs() + .catch(() => { + expect(getConfigStub.calledWith('yieldmoSyntheticInventory')).to.equal(true); + expect(getConfigStub.calledWith('consentManagement')).to.equal(true); + }); + }); + + it('should throw an error if config.placementId is missing', function() { + const { placementId, ...rest } = mockedYmConfig; + + expect(function () { + testExports.validateConfig(rest); + }).throw(`${testExports.MODULE_NAME}: placementId required`); + }); + + it('should throw an error if config.adUnitPath is missing', function() { + const { adUnitPath, ...rest } = mockedYmConfig; + + expect(function () { + testExports.validateConfig(rest); + }).throw(`${testExports.MODULE_NAME}: adUnitPath required`); + }); }); - it('should throw an error if adUnitPath is missed', function() { - const {adUnitPath, ...config} = mockedYmConfig; + describe('getConsentData', () => { + it('should always resolves with object contained "cmp" and "usp" keys', () => { + const consentDataMock = { + cmp: null, + usp: null + }; - expect(function () { - validateConfig(config); - }).throw(`${MODULE_NAME}: adUnitPath required`); + return testExports.getConsentData() + .then(consentDataObj => + expect(consentDataObj).to.eql(consentDataMock)); + }); }); - describe('Ajax ad request', () => { + describe('Get ad', () => { let sandbox; const setAjaxStub = (cb) => { const ajaxStub = sandbox.stub().callsFake(cb); sandbox.stub(ajax, 'ajaxBuilder').callsFake(() => ajaxStub); return ajaxStub; - } - + }; const responseData = { data: [{ ads: [{ @@ -94,86 +117,89 @@ describe('Yieldmo Synthetic Inventory Module', function() { sandbox.restore(); }); - it('should open ad request to ad server', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => {}); + it('should make ad request to ad server', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success('', {responseText: '', status: 200}); + }); + + return testExports.getAd(mockedYmConfig, {cmp: null, usp: null}) + .then(res => { expect(ajaxStub.calledOnce).to.be.true }); + }); - init(mockedYmConfig); + it('should throw an error if server returns an error', () => { + const response = {status: 500}; + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.error('', { status: 500 }); + }); - expect((new URL(ajaxStub.getCall(0).args[0])).host).to.be.equal('ads.yieldmo.com'); + return testExports.getAd(mockedYmConfig, {cmp: null, usp: null}) + .catch(err => { + expect(err.message).to.be.equal(`${testExports.MODULE_NAME}: ad server error: ${response.status}`) + }); }); - it('should properly combine ad request query', () => { + it('should properly create ad request url', () => { const title = 'Test title value'; - const ajaxStub = setAjaxStub((url, callbackObj) => {}); + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success('', {responseText: '', status: 200}); + }); const documentStubTitle = sandbox.stub(document, 'title').value(title); const connection = window.navigator.connection || {}; - init(mockedYmConfig); - const queryParams = getQuearyParamsFromUrl(ajaxStub.getCall(0).args[0]); - const timeStamp = queryParams.bust; - - const paramsToCompare = { - title, - _s: '1', - dnt: 'false', - e: '4', - p: mockedYmConfig.placementId, - page_url: window.top.location.href, - pr: window.top.location.href, - bust: timeStamp, - pft: timeStamp, - ct: timeStamp, - connect: typeof connection.effectiveType !== 'undefined' ? connection.effectiveType : undefined, - bwe: typeof connection.downlink !== 'undefined' ? connection.downlink + 'Mb/sec' : undefined, - rtt: typeof connection.rtt !== 'undefined' ? String(connection.rtt) : undefined, - sd: typeof connection.saveData !== 'undefined' ? String(connection.saveData) : undefined, - scrd: String(window.top.devicePixelRatio || 0), - h: String(window.top.screen.height || window.screen.top.availHeight || window.top.outerHeight || window.top.innerHeight || 481), - w: String(window.top.screen.width || window.screen.top.availWidth || window.top.outerWidth || window.top.innerWidth || 321), - }; - - expect(queryParams).to.eql(JSON.parse(JSON.stringify(paramsToCompare))); + return testExports.getAd(mockedYmConfig, {cmp: null, usp: null}) + .then(res => { + const queryParams = getQuearyParamsFromUrl(ajaxStub.getCall(0).args[0]); + const timeStamp = queryParams.bust; + + const paramsToCompare = { + title, + _s: '1', + dnt: 'false', + e: '4', + p: mockedYmConfig.placementId, + page_url: window.top.location.href, + pr: window.top.location.href, + bust: timeStamp, + pft: timeStamp, + ct: timeStamp, + connect: typeof connection.effectiveType !== 'undefined' ? connection.effectiveType : undefined, + bwe: typeof connection.downlink !== 'undefined' ? connection.downlink + 'Mb/sec' : undefined, + rtt: typeof connection.rtt !== 'undefined' ? String(connection.rtt) : undefined, + sd: typeof connection.saveData !== 'undefined' ? String(connection.saveData) : undefined, + scrd: String(window.top.devicePixelRatio || 0), + h: String(window.top.screen.height || window.screen.top.availHeight || window.top.outerHeight || window.top.innerHeight || 481), + w: String(window.top.screen.width || window.screen.top.availWidth || window.top.outerWidth || window.top.innerWidth || 321), + }; + + expect(queryParams).to.eql(JSON.parse(JSON.stringify(paramsToCompare))); + }) + .catch(err => { throw err; }); }); + }); - it('should send ad request to ad server', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => {}); + describe('setAd', () => { + let sandbox; - init(mockedYmConfig); + const setAjaxStub = (cb) => { + const ajaxStub = sandbox.stub().callsFake(cb); + sandbox.stub(ajax, 'ajaxBuilder').callsFake(() => ajaxStub); + return ajaxStub; + } - expect(ajaxStub.calledOnce).to.be.true; + beforeEach(() => { + sandbox = sinon.sandbox.create(); }); - it('should throw an error if can not parse response', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => { - callbackObj.success('', {responseText: '__invalid_JSON__', status: 200}); - }); - - expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: response is not valid JSON'); + afterEach(() => { + sandbox.restore(); }); - it('should throw an error if status is 204', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => { - callbackObj.success('', {status: 204, responseText: '{}'}); - }); - - expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: no content success status'); - }); + it('should set window.top.googletag and window.top.googletag.cmd', () => { + expect(window.top.googletag).to.be.undefined; - it('should throw an error if error_code present in the ad response', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => { - callbackObj.success('', {status: 200, responseText: '{"data": [{"error_code": "NOAD"}]}'}); - }); + testExports.setGoogleTag(); - expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: no ad, error_code: NOAD'); - }); - - it('should store ad response in window object', () => { - const ajaxStub = setAjaxStub((url, callbackObj) => { - callbackObj.success(JSON.stringify(responseData), {status: 200, responseText: JSON.stringify(responseData)}); - }); - - init(mockedYmConfig); - expect(window.top.__ymAds).to.deep.equal(responseData); + expect(window.top.googletag).to.be.eql({cmd: []}); }); it('should add correct googletag.cmd', function() { @@ -184,7 +210,11 @@ describe('Yieldmo Synthetic Inventory Module', function() { callbackObj.success(JSON.stringify(responseData), {status: 200, responseText: '{"data": [{"ads": []}]}'}); }); - init(mockedYmConfig); + testExports.setAd(mockedYmConfig, { + responseText: `{ + "data": [] + }` + }); expect(gtag.cmd.length).to.equal(1); @@ -207,316 +237,34 @@ describe('Yieldmo Synthetic Inventory Module', function() { }); }); - describe('lookupIabConsent', () => { - const callId = Math.random(); - const cmpFunction = sinon.stub(); - const originalXMLHttpRequest = window.XMLHttpRequest; - let requestMock = { - open: sinon.stub(), - send: sinon.stub(), - }; - let clock; - let postMessageStub; - let mathRandomStub; - let addEventListenerStub; + describe('processAdResponse', () => { + it('should throw if ad response has 204 code', () => { + const response = { status: 204 } - beforeEach(() => { - postMessageStub = sinon.stub(window, 'postMessage'); - mathRandomStub = sinon.stub(Math, 'random'); - addEventListenerStub = sinon.stub(window, 'addEventListener'); - - window.XMLHttpRequest = function FakeXMLHttpRequest() { - this.open = requestMock.open; - this.send = requestMock.send; - this.setRequestHeader = () => {}; - }; - - clock = sinon.useFakeTimers(); + expect(() => testExports.processAdResponse(response)) + .to.throw(`${testExports.MODULE_NAME}: ${response.status} - no ad to serve`) }); - afterEach(() => { - window.XMLHttpRequest = originalXMLHttpRequest; + it('should throw if ad response has 204 code', () => { + const response = { status: 200, responseText: '__invalid_json__' } - postMessageStub.restore(); - mathRandomStub.restore(); - addEventListenerStub.restore(); - - cmpFunction.resetBehavior(); - cmpFunction.resetHistory(); - - requestMock.open.resetBehavior(); - requestMock.open.resetHistory(); - requestMock.send.resetBehavior(); - requestMock.send.resetHistory(); - - clock.restore(); - }); - - it('should get cmp function', () => { - window.__tcfapi = cmpFunction; - - init(mockedYmConfig); - - window.__tcfapi = undefined; - - expect(cmpFunction.calledOnceWith('addEventListener', 2)).to.be.true; + expect(() => testExports.processAdResponse(response)) + .to.throw(`${testExports.MODULE_NAME}: JSON validation error`) }); - it('should call api without cmp consent if can not get it', () => { - cmpFunction.callsFake((e, version, callback) => { - callback(undefined, false); - }); - - window.__tcfapi = cmpFunction; - - init(mockedYmConfig); - - window.__tcfapi = undefined; - - expect(requestMock.open.calledOnce).to.be.true; - }); - - it('should add cmp consent string to ad server request params if gdprApplies is false', () => { - const tcfData = { gdprApplies: false, tcString: 'testTcString' }; - - cmpFunction.callsFake((e, version, callback) => { - callback(tcfData, true); - }); - - window.__tcfapi = cmpFunction; - - init(mockedYmConfig); - - window.__tcfapi = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.cmp).to.be.equal(tcfData.tcString); - }); - - it('should add cmp consent string to ad server request params if eventStatus is tcloaded', () => { - const tcfData = { eventStatus: 'tcloaded', tcString: 'testTcString' }; - - cmpFunction.callsFake((e, version, callback) => { - callback(tcfData, true); - }); - - window.__tcfapi = cmpFunction; - - init(mockedYmConfig); - - window.__tcfapi = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.cmp).to.be.equal(tcfData.tcString); - }); - - it('should add cmp consent string to ad server request params if eventStatus is useractioncomplete', () => { - const tcfData = { eventStatus: 'useractioncomplete', tcString: 'testTcString' }; - - cmpFunction.callsFake((e, version, callback) => { - callback(tcfData, true); - }); - - window.__tcfapi = cmpFunction; - - init(mockedYmConfig); - - window.__tcfapi = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.cmp).to.be.equal(tcfData.tcString); - }); - - it('should post message if cmp consent is loaded from another iframe', () => { - window.frames['__tcfapiLocator'] = 'cmpframe'; - - init(mockedYmConfig); - - window.frames['__tcfapiLocator'] = undefined; - - expect(window.postMessage.callCount).to.be.equal(1); - }); - - it('should add event listener for message event if usp consent is loaded from another iframe', () => { - window.frames['__tcfapiLocator'] = 'cmpframe'; - - init(mockedYmConfig); - - window.frames['__tcfapiLocator'] = undefined; - - expect(window.addEventListener.calledOnceWith('message')).to.be.true; - }); - - it('should add cmp consent string to ad server request params when called from iframe', () => { - const callId = Math.random(); - const tcfData = { gdprApplies: false, tcString: 'testTcString' }; - const cmpEvent = { - data: { - __tcfapiReturn: { - callId: `${callId}`, - returnValue: tcfData, - success: true, - } - }, - }; - - mathRandomStub.returns(callId); - addEventListenerStub.callsFake( - (e, callback) => { - callback(cmpEvent) - } - ); - - window.frames['__tcfapiLocator'] = 'cmpframe'; - - init(mockedYmConfig); - - window.frames['__tcfapiLocator'] = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.cmp).to.be.equal(tcfData.tcString); - }); - }); - - describe('lookupUspConsent', () => { - const callId = Math.random(); - const uspFunction = sinon.stub(); - const originalXMLHttpRequest = window.XMLHttpRequest; - let requestMock = { - open: sinon.stub(), - send: sinon.stub(), - }; - let clock; - let postMessageStub; - let mathRandomStub; - let addEventListenerStub; - - beforeEach(() => { - postMessageStub = sinon.stub(window, 'postMessage'); - mathRandomStub = sinon.stub(Math, 'random'); - addEventListenerStub = sinon.stub(window, 'addEventListener'); - - window.XMLHttpRequest = function FakeXMLHttpRequest() { - this.open = requestMock.open; - this.send = requestMock.send; - this.setRequestHeader = () => {}; - }; - - clock = sinon.useFakeTimers(); - }); - - afterEach(() => { - window.XMLHttpRequest = originalXMLHttpRequest; - - postMessageStub.restore(); - mathRandomStub.restore(); - addEventListenerStub.restore(); - - uspFunction.resetBehavior(); - uspFunction.resetHistory(); - - requestMock.open.resetBehavior(); - requestMock.open.resetHistory(); - requestMock.send.resetBehavior(); - requestMock.send.resetHistory(); - - clock.restore(); - }); - - it('should get cmp function', () => { - window.__uspapi = uspFunction; - - init(mockedYmConfig); - - window.__uspapi = undefined; - - expect(uspFunction.calledOnceWith('getUSPData', 1)).to.be.true; - }); - - it('should call api without usp consent if can not get it', () => { - uspFunction.callsFake((e, version, callback) => { - callback(undefined, false); - }); - - window.__uspapi = uspFunction; - - init(mockedYmConfig); - - window.__uspapi = undefined; - - expect(requestMock.open.calledOnce).to.be.true; - }); - - it('should add usp consent string to ad server request params', () => { - const uspData = { uspString: 'testUspString' }; - - uspFunction.callsFake((e, version, callback) => { - callback(uspData, true); - }); - - window.__uspapi = uspFunction; - - init(mockedYmConfig); - - window.__uspapi = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.us_privacy).to.be.equal(uspData.uspString); - }); - - it('should post message if usp consent is loaded from another iframe', () => { - window.frames['__uspapiLocator'] = 'uspframe'; - - init(mockedYmConfig); - - window.frames['__uspapiLocator'] = undefined; - - expect(window.postMessage.callCount).to.be.equal(1); - }); - - it('should add event listener for message event if usp consent is loaded from another iframe', () => { - window.frames['__uspapiLocator'] = 'uspframe'; - - init(mockedYmConfig); - - window.frames['__uspapiLocator'] = undefined; - - expect(window.addEventListener.calledOnceWith('message')).to.be.true; - }); - - it('should add usp consent string to ad server request params when called from iframe', () => { - const uspData = { uspString: 'testUspString' }; - const uspEvent = { - data: { - __uspapiReturn: { - callId: `${callId}`, - returnValue: uspData, - success: true, - } - }, + it('should throw if ad response has error_code', () => { + const response = { + responseText: `{ + "data": [ + { + "error_code": "NOAD" + } + ] + }` }; - mathRandomStub.returns(callId); - addEventListenerStub.callsFake( - (e, callback) => { - callback(uspEvent) - } - ); - - window.frames['__uspapiLocator'] = 'cmpframe'; - - init(mockedYmConfig); - - window.frames['__uspapiLocator'] = undefined; - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - - expect(queryParams.us_privacy).to.be.equal(uspData.uspString); + expect(() => testExports.processAdResponse(response)) + .to.throw(`${testExports.MODULE_NAME}: no ad, error_code: ${JSON.parse(response.responseText).data[0].error_code}`) }); }); });