diff --git a/integrationExamples/gpt/hello_world_emoteev.html b/integrationExamples/gpt/hello_world_emoteev.html index 5a33e2d9701..f41ef308332 100644 --- a/integrationExamples/gpt/hello_world_emoteev.html +++ b/integrationExamples/gpt/hello_world_emoteev.html @@ -5,68 +5,99 @@ @@ -75,9 +106,9 @@

Basic Prebid.js Example

Div-1
diff --git a/modules/emoteevBidAdapter.js b/modules/emoteevBidAdapter.js index 4436d39bb70..db84b6ea36d 100644 --- a/modules/emoteevBidAdapter.js +++ b/modules/emoteevBidAdapter.js @@ -22,11 +22,12 @@ import { contains, deepAccess, isArray, - getParameterByName + isInteger, + getParameterByName, + getCookie } from '../src/utils'; import {config} from '../src/config'; import * as url from '../src/url'; -import {getCookie} from './pubCommonId'; export const BIDDER_CODE = 'emoteev'; @@ -60,6 +61,19 @@ export const ON_ADAPTER_CALLED = 'on_adapter_called'; export const ON_BID_WON = 'on_bid_won'; export const ON_BIDDER_TIMEOUT = 'on_bidder_timeout'; +export const IN_CONTENT = 'content'; +export const FOOTER = 'footer'; +export const OVERLAY = 'overlay'; +export const WALLPAPER = 'wallpaper'; + +/** + * Vendor ID assigned to Emoteev from the Global Vendor & CMP List. + * + * See https://vendorlist.consensu.org/vendorinfo.json for more information. + * @type {number} + */ +export const VENDOR_ID = 15; + /** * Pure function. See http://prebid.org/dev-docs/bidder-adaptor.html#valid-build-requests-array for detailed semantic. * @@ -71,6 +85,8 @@ export const isBidRequestValid = (bidRequest) => { bidRequest && bidRequest.params && deepAccess(bidRequest, 'params.adSpaceId') && + validateContext(deepAccess(bidRequest, 'params.context')) && + validateExternalId(deepAccess(bidRequest, 'params.externalId')) && bidRequest.bidder === BIDDER_CODE && validateSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'))); }; @@ -89,7 +105,7 @@ export const buildRequests = (env, debug, currency, validBidRequests, bidderRequ return { method: 'POST', url: bidderUrl(env), - data: JSON.stringify(requestsPayload(debug, currency, validBidRequests, bidderRequest)) + data: JSON.stringify(requestsPayload(debug, currency, validBidRequests, bidderRequest)) // Keys with undefined values will be filtered out. }; }; @@ -264,7 +280,23 @@ export const userSyncImageUrl = env => url.format({ * @param {Array>} sizes * @returns {boolean} are sizes valid? */ -const validateSizes = sizes => isArray(sizes) && sizes.some(size => isArray(size) && size.length === 2); +export const validateSizes = sizes => isArray(sizes) && sizes.length > 0 && sizes.every(size => isArray(size) && size.length === 2); + +/** + * Pure function. + * + * @param {string} context + * @returns {boolean} is param `context` valid? + */ +export const validateContext = context => contains([IN_CONTENT, FOOTER, OVERLAY, WALLPAPER], context); + +/** + * Pure function. + * + * @param {(number|null|undefined)} externalId + * @returns {boolean} is param `externalId` valid? + */ +export const validateExternalId = externalId => externalId === undefined || externalId === null || (isInteger(externalId) && externalId > 0); /** * Pure function. @@ -282,6 +314,14 @@ export const conformBidRequest = bidRequest => { }; }; +/** + * Pure function. + * + * @param {object} bidderRequest + * @returns {(boolean|undefined)} raw consent data. + */ +export const gdprConsent = (bidderRequest) => (deepAccess(bidderRequest, 'gdprConsent.vendorData.vendorConsents') || {})[VENDOR_ID]; + /** * Pure function. * @@ -306,7 +346,7 @@ export const requestsPayload = (debug, currency, validBidRequests, bidderRequest isWebGLEnabled(document)), userAgent: navigator.userAgent, gdprApplies: deepAccess(bidderRequest, 'gdprConsent.gdprApplies'), - gdprConsent: deepAccess(bidderRequest, 'gdprConsent.consentString'), + gdprConsent: gdprConsent(bidderRequest), }; }; @@ -426,7 +466,7 @@ export const getDeviceInfo = (deviceDimensions, viewDimensions, documentDimensio * Pure function * @param {object} config pbjs config value * @param {string} parameter Environment override from URL query param. - * @returns One of [PRODUCTION, STAGING, DEVELOPMENT]. + * @returns {string} One of [PRODUCTION, STAGING, DEVELOPMENT]. */ export const resolveEnv = (config, parameter) => { const configEnv = deepAccess(config, 'emoteev.env'); diff --git a/modules/emoteevBidAdapter.md b/modules/emoteevBidAdapter.md index 88b0b21a96f..226a8374369 100644 --- a/modules/emoteevBidAdapter.md +++ b/modules/emoteevBidAdapter.md @@ -18,14 +18,16 @@ Module that connects to Emoteev's demand sources code: 'test-div', mediaTypes: { banner: { - sizes: [[300, 250]], + sizes: [[720, 90]], } }, bids: [ { bidder: 'emoteev', params: { - adSpaceId: 5084 + adSpaceId: 5084, + context: 'footer', + externalId: 42, } } ] diff --git a/test/spec/modules/emoteevBidAdapter_spec.js b/test/spec/modules/emoteevBidAdapter_spec.js index a5460ab939d..aa97b58ec38 100644 --- a/test/spec/modules/emoteevBidAdapter_spec.js +++ b/test/spec/modules/emoteevBidAdapter_spec.js @@ -15,20 +15,23 @@ import { DEVELOPMENT, EVENTS_PATH, eventsUrl, + FOOTER, + gdprConsent, getDeviceDimensions, getDeviceInfo, getDocumentDimensions, getUserSyncs, getViewDimensions, + IN_CONTENT, interpretResponse, isBidRequestValid, - isWebGLEnabled, ON_ADAPTER_CALLED, ON_BID_WON, ON_BIDDER_TIMEOUT, onBidWon, onAdapterCalled, onTimeout, + OVERLAY, PRODUCTION, requestsPayload, resolveDebug, @@ -39,10 +42,14 @@ import { USER_SYNC_IMAGE_PATH, userSyncIframeUrl, userSyncImageUrl, + validateSizes, + validateContext, + validateExternalId, + VENDOR_ID, + WALLPAPER, } from 'modules/emoteevBidAdapter'; import * as url from '../../../src/url'; import * as utils from '../../../src/utils'; -import * as pubCommonId from '../../../modules/pubCommonId'; import {config} from '../../../src/config'; const cannedValidBidRequests = [{ @@ -53,7 +60,11 @@ const cannedValidBidRequests = [{ bidder: 'emoteev', bidderRequestId: '1203b39fecc6a5', crumbs: {pubcid: 'f3371d16-4e8b-42b5-a770-7e5be1fdf03d'}, - params: {adSpaceId: 5084}, + params: { + adSpaceId: 5084, + context: IN_CONTENT, + externalId: 42 + }, sizes: [[300, 250], [250, 300], [300, 600]], transactionId: '58dbd732-7a39-45f1-b23e-1c24051a941c', }]; @@ -74,7 +85,7 @@ const cannedBidderRequest = { timeout: 3000, gdprConsent: { gdprApplies: true, - consentString: 'my consentString' + vendorData: {vendorConsents: {[VENDOR_ID]: true}}, } }; const serverResponse = @@ -102,6 +113,8 @@ describe('emoteevBidAdapter', function () { bidId: '23a45b4e3', params: { adSpaceId: 12345, + context: IN_CONTENT, + externalId: 42 }, mediaTypes: { banner: { @@ -120,6 +133,8 @@ describe('emoteevBidAdapter', function () { bidder: '', // invalid bidder params: { adSpaceId: 12345, + context: IN_CONTENT, + externalId: 42 }, mediaTypes: { banner: { @@ -131,6 +146,21 @@ describe('emoteevBidAdapter', function () { bidder: 'emoteev', params: { adSpaceId: '', // invalid adSpaceId + context: IN_CONTENT, + externalId: 42 + }, + mediaTypes: { + banner: { + sizes: [[750, 200]] + } + }, + })).to.equal(false); + expect(isBidRequestValid({ + bidder: 'emoteev', + params: { + adSpaceId: 12345, + context: 'something', // invalid context + externalId: 42 }, mediaTypes: { banner: { @@ -142,6 +172,21 @@ describe('emoteevBidAdapter', function () { bidder: 'emoteev', params: { adSpaceId: 12345, + context: IN_CONTENT, + externalId: 'lol' // invalid externalId + }, + mediaTypes: { + banner: { + sizes: [[750, 200]] + } + }, + })).to.equal(false); + expect(isBidRequestValid({ + bidder: 'emoteev', + params: { + adSpaceId: 12345, + context: IN_CONTENT, + externalId: 42 }, mediaTypes: { banner: { @@ -401,6 +446,39 @@ describe('emoteevBidAdapter', function () { }); }); + describe('gdprConsent', function () { + describe('gdpr applies, consent given', function () { + const bidderRequest = { + ...cannedBidderRequest, + gdprConsent: { + gdprApplies: true, + vendorData: {vendorConsents: {[VENDOR_ID]: true}}, + } + }; + expect(gdprConsent(bidderRequest)).to.deep.equal(true); + }); + describe('gdpr applies, consent withdrawn', function () { + const bidderRequest = { + ...cannedBidderRequest, + gdprConsent: { + gdprApplies: true, + vendorData: {vendorConsents: {[VENDOR_ID]: false}}, + } + }; + expect(gdprConsent(bidderRequest)).to.deep.equal(false); + }); + describe('gdpr applies, consent unknown', function () { + const bidderRequest = { + ...cannedBidderRequest, + gdprConsent: { + gdprApplies: true, + vendorData: {}, + } + }; + expect(gdprConsent(bidderRequest)).to.deep.equal(undefined); + }); + }); + describe('requestsPayload', function () { const currency = 'EUR', @@ -418,7 +496,7 @@ describe('emoteevBidAdapter', function () { 'deviceInfo', 'userAgent', 'gdprApplies', - 'gdprConsent' + 'gdprConsent', ); expect(payload.bidRequests[0]).to.exist.and.have.all.keys( @@ -449,7 +527,6 @@ describe('emoteevBidAdapter', function () { ); expect(payload.userAgent).to.deep.equal(navigator.userAgent); expect(payload.gdprApplies).to.deep.equal(cannedBidderRequest.gdprConsent.gdprApplies); - expect(payload.gdprConsent).to.deep.equal(cannedBidderRequest.gdprConsent.consentString); }); describe('getViewDimensions', function () { @@ -665,7 +742,7 @@ describe('emoteevBidAdapter', function () { let getParameterByNameSpy; beforeEach(function () { triggerPixelSpy = sinon.spy(utils, 'triggerPixel'); - getCookieSpy = sinon.spy(pubCommonId, 'getCookie'); + getCookieSpy = sinon.spy(utils, 'getCookie'); getConfigSpy = sinon.spy(config, 'getConfig'); getParameterByNameSpy = sinon.spy(utils, 'getParameterByName'); }); @@ -692,7 +769,7 @@ describe('emoteevBidAdapter', function () { }; spec.isBidRequestValid(validBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -700,7 +777,7 @@ describe('emoteevBidAdapter', function () { const invalidBidRequest = {}; spec.isBidRequestValid(invalidBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -709,7 +786,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.buildRequests(cannedValidBidRequests, cannedBidderRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.callCount(config.getConfig, 3); sinon.assert.callCount(utils.getParameterByName, 2); }); @@ -718,7 +795,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.interpretResponse(serverResponse); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -728,7 +805,7 @@ describe('emoteevBidAdapter', function () { const bidObject = serverResponse.body[0]; spec.onBidWon(bidObject); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.calledOnce(pubCommonId.getCookie); + sinon.assert.calledOnce(utils.getCookie); sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -737,7 +814,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.onTimeout(cannedValidBidRequests[0]); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -746,10 +823,44 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.getUserSyncs({}); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(pubCommonId.getCookie); + sinon.assert.notCalled(utils.getCookie); sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); }); }); + + describe('validateSizes', function () { + it('only accepts valid array of sizes', function () { + expect(validateSizes([])).to.deep.equal(false); + expect(validateSizes([[]])).to.deep.equal(false); + expect(validateSizes([[450, 450], undefined])).to.deep.equal(false); + expect(validateSizes([[450, 450], 'size'])).to.deep.equal(false); + expect(validateSizes([[1, 1]])).to.deep.equal(true); + expect(validateSizes([[1, 1], [450, 450]])).to.deep.equal(true); + }); + }); + + describe('validateContext', function () { + it('only accepts valid context', function () { + expect(validateContext(IN_CONTENT)).to.deep.equal(true); + expect(validateContext(FOOTER)).to.deep.equal(true); + expect(validateContext(OVERLAY)).to.deep.equal(true); + expect(validateContext(WALLPAPER)).to.deep.equal(true); + expect(validateContext(null)).to.deep.equal(false); + expect(validateContext('anything else')).to.deep.equal(false); + }); + }); + + describe('validateExternalId', function () { + it('only accepts a positive integer or null', function () { + expect(validateExternalId(0)).to.deep.equal(false); + expect(validateExternalId(42)).to.deep.equal(true); + expect(validateExternalId(42.0)).to.deep.equal(true); // edge case: valid externalId + expect(validateExternalId(3.14159)).to.deep.equal(false); + expect(validateExternalId('externalId')).to.deep.equal(false); + expect(validateExternalId(undefined)).to.deep.equal(true); + expect(validateExternalId(null)).to.deep.equal(true); + }); + }); });