diff --git a/integrationExamples/gpt/1plusXRtdProviderExample.html b/integrationExamples/gpt/1plusXRtdProviderExample.html new file mode 100644 index 00000000000..2eb75063df1 --- /dev/null +++ b/integrationExamples/gpt/1plusXRtdProviderExample.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + +

1plusX RTD Module for Prebid

+ +
+ +
+ + + \ No newline at end of file diff --git a/modules/.submodules.json b/modules/.submodules.json index d808b10051b..d24e7ff96f5 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -49,6 +49,7 @@ "dfpAdServerVideo" ], "rtdModule": [ + "1plusXRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", "blueconicRtdProvider", @@ -84,4 +85,4 @@ ] } } -} +} \ No newline at end of file diff --git a/modules/1plusXRtdProvider.js b/modules/1plusXRtdProvider.js new file mode 100644 index 00000000000..5affafcf9d3 --- /dev/null +++ b/modules/1plusXRtdProvider.js @@ -0,0 +1,251 @@ +import { submodule } from '../src/hook.js'; +import { config } from '../src/config.js'; +import { ajax } from '../src/ajax.js'; +import { + logMessage, logError, + deepAccess, mergeDeep, + isNumber, isArray, deepSetValue +} from '../src/utils.js'; + +// Constants +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = '1plusX'; +const ORTB2_NAME = '1plusX.com' +const PAPI_VERSION = 'v1.0'; +const LOG_PREFIX = '[1plusX RTD Module]: '; +const LEGACY_SITE_KEYWORDS_BIDDERS = ['appnexus']; +export const segtaxes = { + // cf. https://github.com/InteractiveAdvertisingBureau/openrtb/pull/108 + AUDIENCE: 526, + CONTENT: 527, +}; +// Functions +/** + * Extracts the parameters for 1plusX RTD module from the config object passed at instanciation + * @param {Object} moduleConfig Config object passed to the module + * @param {Object} reqBidsConfigObj Config object for the bidders; each adapter has its own entry + * @returns {Object} Extracted configuration parameters for the module + */ +export const extractConfig = (moduleConfig, reqBidsConfigObj) => { + // CustomerId + const customerId = deepAccess(moduleConfig, 'params.customerId'); + if (!customerId) { + throw new Error('Missing parameter customerId in moduleConfig'); + } + // Timeout + const tempTimeout = deepAccess(moduleConfig, 'params.timeout'); + const timeout = isNumber(tempTimeout) && tempTimeout > 300 ? tempTimeout : 1000; + + // Bidders + const biddersTemp = deepAccess(moduleConfig, 'params.bidders'); + if (!isArray(biddersTemp) || !biddersTemp.length) { + throw new Error('Missing parameter bidders in moduleConfig'); + } + + const adUnitBidders = reqBidsConfigObj.adUnits + .flatMap(({ bids }) => bids.map(({ bidder }) => bidder)) + .filter((e, i, a) => a.indexOf(e) === i); + if (!isArray(adUnitBidders) || !adUnitBidders.length) { + throw new Error('Missing parameter bidders in bidRequestConfig'); + } + + const bidders = biddersTemp.filter(bidder => adUnitBidders.includes(bidder)); + if (!bidders.length) { + throw new Error('No bidRequestConfig bidder found in moduleConfig bidders'); + } + + return { customerId, timeout, bidders }; +} + +/** + * Gets the URL of Profile Api from which targeting data will be fetched + * @param {Object} config + * @param {string} config.customerId + * @returns {string} URL to access 1plusX Profile API + */ +const getPapiUrl = ({ customerId }) => { + // https://[yourClientId].profiles.tagger.opecloud.com/[VERSION]/targeting?url= + const currentUrl = encodeURIComponent(window.location.href); + const papiUrl = `https://${customerId}.profiles.tagger.opecloud.com/${PAPI_VERSION}/targeting?url=${currentUrl}`; + return papiUrl; +} + +/** + * Fetches targeting data. It contains the audience segments & the contextual topics + * @param {string} papiUrl URL of profile API + * @returns {Promise} Promise object resolving with data fetched from Profile API + */ +const getTargetingDataFromPapi = (papiUrl) => { + return new Promise((resolve, reject) => { + const requestOptions = { + customHeaders: { + 'Accept': 'application/json' + } + } + const callbacks = { + success(responseText, response) { + resolve(JSON.parse(response.response)); + }, + error(error) { + reject(error); + } + }; + ajax(papiUrl, callbacks, null, requestOptions) + }) +} + +/** + * Prepares the update for the ORTB2 object + * @param {Object} targetingData Targeting data fetched from Profile API + * @param {string[]} segments Represents the audience segments of the user + * @param {string[]} topics Represents the topics of the page + * @returns {Object} Object describing the updates to make on bidder configs + */ +export const buildOrtb2Updates = ({ segments = [], topics = [] }, bidder) => { + // Currently appnexus bidAdapter doesn't support topics in `site.content.data.segment` + // Therefore, writing them in `site.keywords` until it's supported + // Other bidAdapters do fine with `site.content.data.segment` + const writeToLegacySiteKeywords = LEGACY_SITE_KEYWORDS_BIDDERS.includes(bidder); + if (writeToLegacySiteKeywords) { + const site = { + keywords: topics.join(',') + }; + return { site }; + } + + const userData = { + name: ORTB2_NAME, + segment: segments.map((segmentId) => ({ id: segmentId })) + }; + const siteContentData = { + name: ORTB2_NAME, + segment: topics.map((topicId) => ({ id: topicId })), + ext: { segtax: segtaxes.CONTENT } + } + return { userData, siteContentData }; +} + +/** + * Merges the targeting data with the existing config for bidder and updates + * @param {string} bidder Bidder for which to set config + * @param {Object} ortb2Updates Updates to be applied to bidder config + * @param {Object} bidderConfigs All current bidder configs + * @returns {Object} Updated bidder config + */ +export const updateBidderConfig = (bidder, ortb2Updates, bidderConfigs) => { + const { site, siteContentData, userData } = ortb2Updates; + const bidderConfigCopy = mergeDeep({}, bidderConfigs[bidder]); + + if (site) { + // Legacy : cf. comment on buildOrtb2Updates first lines + const currentSite = deepAccess(bidderConfigCopy, 'ortb2.site') + const updatedSite = mergeDeep(currentSite, site); + deepSetValue(bidderConfigCopy, 'ortb2.site', updatedSite); + } + + if (siteContentData) { + const siteDataPath = 'ortb2.site.content.data'; + const currentSiteContentData = deepAccess(bidderConfigCopy, siteDataPath) || []; + const updatedSiteContentData = [ + ...currentSiteContentData.filter(({ name }) => name != siteContentData.name), + siteContentData + ]; + deepSetValue(bidderConfigCopy, siteDataPath, updatedSiteContentData); + } + + if (userData) { + const userDataPath = 'ortb2.user.data'; + const currentUserData = deepAccess(bidderConfigCopy, userDataPath) || []; + const updatedUserData = [ + ...currentUserData.filter(({ name }) => name != userData.name), + userData + ]; + deepSetValue(bidderConfigCopy, userDataPath, updatedUserData); + } + + return bidderConfigCopy; +}; + +const setAppnexusAudiences = (audiences) => { + config.setConfig({ + appnexusAuctionKeywords: { + '1plusX': audiences, + }, + }); +} + +/** + * Updates bidder configs with the targeting data retreived from Profile API + * @param {Object} papiResponse Response from Profile API + * @param {Object} config Module configuration + * @param {string[]} config.bidders Bidders specified in module's configuration + */ +export const setTargetingDataToConfig = (papiResponse, { bidders }) => { + const bidderConfigs = config.getBidderConfig(); + const { s: segments, t: topics } = papiResponse; + + for (const bidder of bidders) { + const ortb2Updates = buildOrtb2Updates({ segments, topics }, bidder); + const updatedBidderConfig = updateBidderConfig(bidder, ortb2Updates, bidderConfigs); + if (updatedBidderConfig) { + config.setBidderConfig({ + bidders: [bidder], + config: updatedBidderConfig + }); + } + if (bidder === 'appnexus') { + // Do the legacy stuff for appnexus with segments + setAppnexusAudiences(segments); + } + } +} + +// Functions exported in submodule object +/** + * Init + * @param {Object} config Module configuration + * @param {boolean} userConsent + * @returns true + */ +const init = (config, userConsent) => { + return true; +} + +/** + * + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} moduleConfig Configuration for 1plusX RTD module + * @param {boolean} userConsent + */ +const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + try { + // Get the required config + const { customerId, bidders } = extractConfig(moduleConfig, reqBidsConfigObj); + // Get PAPI URL + const papiUrl = getPapiUrl({ customerId }) + // Call PAPI + getTargetingDataFromPapi(papiUrl) + .then((papiResponse) => { + logMessage(LOG_PREFIX, 'Get targeting data request successful'); + setTargetingDataToConfig(papiResponse, { bidders }); + callback(); + }) + .catch((error) => { + throw error; + }) + } catch (error) { + logError(LOG_PREFIX, error); + callback(); + } +} + +// The RTD submodule object to be exported +export const onePlusXSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData +} + +// Register the onePlusXSubmodule as submodule of realTimeData +submodule(REAL_TIME_MODULE, onePlusXSubmodule); diff --git a/modules/1plusXRtdProvider.md b/modules/1plusXRtdProvider.md new file mode 100644 index 00000000000..75ad3b966d1 --- /dev/null +++ b/modules/1plusXRtdProvider.md @@ -0,0 +1,67 @@ +# 1plusX Real-time Data Submodule + +## Overview + + Module Name: 1plusX Rtd Provider + Module Type: Rtd Provider + Maintainer: dc-team-1px@triplelift.com + +## Description + +RTD provider for 1plusX. +Enriches the bidding object with Audience & Targeting data +Contact dc-team-1px@triplelift.com for information. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,1plusXRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the 1plusX RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initilize the 1plusX RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +var TIMEOUT = 1000; +pbjs.setConfig({ + realTimeData: { + auctionDelay: TIMEOUT, + dataProviders: [{ + name: '1plusX', + waitForIt: true, + params: { + customerId: 'acme', + bidders: ['appnexus', 'rubicon'], + timeout: TIMEOUT + } + }] + } +}); +``` + +### Parameters + +| Name | Type | Description | Notes | +| :---------------- | :------------ | :--------------------------------------------------------------- |:-------------------------------------------------------- | +| name | String | Real time data module name | Always '1plusX' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.customerId | String | Your 1plusX customer id | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.timeout | Integer | timeout (ms) | 1000ms | + +## Testing + +To view an example of how the 1plusX RTD module works : + +`gulp serve --modules=rtdModule,1plusXRtdProvider,appnexusBidAdapter,rubiconBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/1plusXRtdProvider_example.html` diff --git a/test/spec/modules/1plusXRtdProvider_spec.js b/test/spec/modules/1plusXRtdProvider_spec.js new file mode 100644 index 00000000000..9682e4b62f8 --- /dev/null +++ b/test/spec/modules/1plusXRtdProvider_spec.js @@ -0,0 +1,430 @@ +import { config } from 'src/config'; +import { + onePlusXSubmodule, + segtaxes, + extractConfig, + buildOrtb2Updates, + updateBidderConfig, + setTargetingDataToConfig +} from 'modules/1plusXRtdProvider'; + +describe('1plusXRtdProvider', () => { + // Fake server config + let fakeServer; + const fakeResponseHeaders = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }; + const fakeResponse = { + s: ['segment1', 'segment2', 'segment3'], + t: ['targeting1', 'targeting2', 'targeting3'] + }; + + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'appnexus' } + ] + }] + }; + + // Bidder configs + const bidderConfigInitial = { + ortb2: { + user: { keywords: '' }, + site: { ext: {} } + } + } + const bidderConfigInitialWith1plusXUserData = { + ortb2: { + user: { + data: [{ name: '1plusX.com', segment: [{ id: 'initial' }] }] + }, + site: { content: { data: [] } } + } + } + const bidderConfigInitialWithUserData = { + ortb2: { + user: { + data: [{ name: 'hello.world', segment: [{ id: 'initial' }] }] + }, + site: { content: { data: [] } } + } + } + const bidderConfigInitialWith1plusXSiteContent = { + ortb2: { + user: { data: [] }, + site: { + content: { + data: [{ + name: '1plusX.com', segment: [{ id: 'initial' }], ext: { segtax: 525 } + }] + } + }, + } + } + const bidderConfigInitialWithSiteContent = { + ortb2: { + user: { data: [] }, + site: { + content: { + data: [{ name: 'hello.world', segment: [{ id: 'initial' }] }] + } + }, + } + } + // Util functions + const randomBidder = (len = 5) => Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, len); + + before(() => { + config.resetConfig(); + }) + + after(() => { }) + + beforeEach(() => { + fakeServer = sinon.createFakeServer(); + fakeServer.respondWith('GET', '*', [200, fakeResponseHeaders, JSON.stringify(fakeResponse)]); + fakeServer.respondImmediately = true; + fakeServer.autoRespond = true; + }) + + describe('onePlusXSubmodule', () => { + it('init is successfull', () => { + const initResult = onePlusXSubmodule.init(); + expect(initResult).to.be.true; + }) + + it('callback is called after getBidRequestData', () => { + // Nice case; everything runs as expected + { + const callbackSpy = sinon.spy(); + const config = { params: { customerId: 'test', bidders: ['appnexus'] } }; + onePlusXSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, config); + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true + }, 100) + } + // No customer id in config => error but still callback called + { + const callbackSpy = sinon.spy(); + const config = {} + onePlusXSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, config); + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true + }, 100); + } + // No bidders in config => error but still callback called + { + const callbackSpy = sinon.spy(); + const config = { customerId: 'test' } + onePlusXSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, config); + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true + }, 100); + } + }) + }) + + describe('extractConfig', () => { + const customerId = 'test'; + const timeout = 1000; + const bidders = ['appnexus']; + + it('Throws an error if no customerId is specified', () => { + const moduleConfig = { params: { timeout, bidders } }; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }) + it('Throws an error if no bidder is specified', () => { + const moduleConfig = { params: { customerId, timeout } }; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }) + it("Throws an error if there's no bidder in reqBidsConfigObj", () => { + const moduleConfig = { params: { customerId, timeout, bidders } }; + const reqBidsConfigEmpty = { adUnits: [{ bids: [] }] }; + expect(() => extractConfig(moduleConfig, reqBidsConfigEmpty)).to.throw(); + }) + it('Returns an object containing the parameters specified', () => { + const moduleConfig = { params: { customerId, timeout, bidders } }; + const expectedKeys = ['customerId', 'timeout', 'bidders'] + const extractedConfig = extractConfig(moduleConfig, reqBidsConfigObj); + expect(extractedConfig).to.be.an('object').and.to.have.all.keys(expectedKeys); + expect(extractedConfig.customerId).to.equal(customerId); + expect(extractedConfig.timeout).to.equal(timeout); + expect(extractedConfig.bidders).to.deep.equal(bidders); + }) + /* 1plusX RTD module may only use bidders that are both specified in : + - the bid request configuration + - AND in the 1plusX RTD module configuration + Below 2 tests are enforcing those rules + */ + it('Returns the intersection of bidders found in bid request config & module config', () => { + const bidders = ['appnexus', 'rubicon']; + const moduleConfig = { params: { customerId, timeout, bidders } }; + const { bidders: extractedBidders } = extractConfig(moduleConfig, reqBidsConfigObj); + expect(extractedBidders).to.be.an('array').and.to.have.length(1); 7 + expect(extractedBidders[0]).to.equal('appnexus'); + }) + it('Throws an error if no bidder can be used by the module', () => { + const bidders = ['rubicon']; + const moduleConfig = { params: { customerId, timeout, bidders } }; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }) + }) + + describe('buildOrtb2Updates', () => { + it('fills site.content.data & user.data in the ortb2 config', () => { + const rtdData = { segments: fakeResponse.s, topics: fakeResponse.t }; + const ortb2Updates = buildOrtb2Updates(rtdData, randomBidder()); + + const expectedOutput = { + siteContentData: { + name: '1plusX.com', + segment: rtdData.topics.map((topicId) => ({ id: topicId })), + ext: { segtax: segtaxes.CONTENT } + }, + userData: { + name: '1plusX.com', + segment: rtdData.segments.map((segmentId) => ({ id: segmentId })) + } + } + expect([ortb2Updates]).to.deep.include.members([expectedOutput]); + }); + it('fills site.keywords in the ortb2 config (appnexus specific)', () => { + const rtdData = { segments: fakeResponse.s, topics: fakeResponse.t }; + const ortb2Updates = buildOrtb2Updates(rtdData, 'appnexus'); + + const expectedOutput = { + site: { + keywords: rtdData.topics.join(','), + } + } + expect([ortb2Updates]).to.deep.include.members([expectedOutput]); + }); + + it('defaults to empty array if no segment is given', () => { + const rtdData = { topics: fakeResponse.t }; + const ortb2Updates = buildOrtb2Updates(rtdData, randomBidder()); + + const expectedOutput = { + siteContentData: { + name: '1plusX.com', + segment: rtdData.topics.map((topicId) => ({ id: topicId })), + ext: { segtax: segtaxes.CONTENT } + }, + userData: { + name: '1plusX.com', + segment: [] + } + } + expect(ortb2Updates).to.deep.include(expectedOutput); + }) + + it('defaults to empty array if no topic is given', () => { + const rtdData = { segments: fakeResponse.s }; + const ortb2Updates = buildOrtb2Updates(rtdData, randomBidder()); + + const expectedOutput = { + siteContentData: { + name: '1plusX.com', + segment: [], + ext: { segtax: segtaxes.CONTENT } + }, + userData: { + name: '1plusX.com', + segment: rtdData.segments.map((segmentId) => ({ id: segmentId })) + } + } + expect(ortb2Updates).to.deep.include(expectedOutput); + }) + it('defaults to empty string if no topic is given (appnexus specific)', () => { + const rtdData = { segments: fakeResponse.s }; + const ortb2Updates = buildOrtb2Updates(rtdData, 'appnexus'); + + const expectedOutput = { + site: { + keywords: '', + } + } + expect(ortb2Updates).to.deep.include(expectedOutput); + }) + }) + + describe('updateBidderConfig', () => { + const ortb2UpdatesAppNexus = { + site: { + keywords: fakeResponse.t.join(','), + }, + userData: { + name: '1plusX.com', + segment: fakeResponse.s.map((segmentId) => ({ id: segmentId })) + } + } + const ortb2Updates = { + siteContentData: { + name: '1plusX.com', + segment: fakeResponse.t.map((topicId) => ({ id: topicId })), + ext: { segtax: segtaxes.CONTENT } + }, + userData: { + name: '1plusX.com', + segment: fakeResponse.s.map((segmentId) => ({ id: segmentId })) + } + } + + it('merges fetched data in bidderConfig for configured bidders', () => { + const bidder = randomBidder(); + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitial + }); + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2Updates, config.getBidderConfig()); + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.site.content.data).to.deep.include(ortb2Updates.siteContentData); + expect(newBidderConfig.ortb2.user.data).to.deep.include(ortb2Updates.userData); + // Check that existing config didn't get erased + expect(newBidderConfig.ortb2.site).to.deep.include(bidderConfigInitial.ortb2.site); + expect(newBidderConfig.ortb2.user).to.deep.include(bidderConfigInitial.ortb2.user); + }) + + it('merges fetched data in bidderConfig for configured bidders (appnexus specific)', () => { + const bidder = 'appnexus'; + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitial + }); + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2UpdatesAppNexus, config.getBidderConfig()); + + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.site).to.deep.include(ortb2UpdatesAppNexus.site); + // Check that existing config didn't get erased + expect(newBidderConfig.ortb2.site).to.deep.include(bidderConfigInitial.ortb2.site); + expect(newBidderConfig.ortb2.user).to.deep.include(bidderConfigInitial.ortb2.user); + }) + + it('overwrites an existing 1plus.com entry in ortb2.user.data', () => { + const bidder = randomBidder(); + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitialWith1plusXUserData + }); + // Save previous user.data entry + const previousUserData = bidderConfigInitialWith1plusXUserData.ortb2.user.data[0]; + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2Updates, config.getBidderConfig()); + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.user.data).to.deep.include(ortb2Updates.userData); + expect(newBidderConfig.ortb2.user.data).not.to.include(previousUserData); + }) + it("doesn't overwrite entries in ortb2.user.data that aren't 1plusx.com", () => { + const bidder = randomBidder(); + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitialWithUserData + }); + // Save previous user.data entry + const previousUserData = bidderConfigInitialWithUserData.ortb2.user.data[0]; + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2Updates, config.getBidderConfig()); + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.user.data).to.deep.include(ortb2Updates.userData); + expect(newBidderConfig.ortb2.user.data).to.deep.include(previousUserData); + }) + + it('overwrites an existing 1plus.com entry in ortb2.site.content.data', () => { + const bidder = randomBidder(); + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitialWith1plusXSiteContent + }); + // Save previous user.data entry + const previousSiteContent = bidderConfigInitialWith1plusXSiteContent.ortb2.site.content.data[0]; + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2Updates, config.getBidderConfig()); + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.site.content.data).to.deep.include(ortb2Updates.siteContentData); + expect(newBidderConfig.ortb2.site.content.data).not.to.include(previousSiteContent); + }) + it("doesn't overwrite entries in ortb2.site.content.data that aren't 1plusx.com", () => { + const bidder = randomBidder(); + // Set initial config + config.setBidderConfig({ + bidders: [bidder], + config: bidderConfigInitialWithSiteContent + }); + // Save previous user.data entry + const previousSiteContent = bidderConfigInitialWithSiteContent.ortb2.site.content.data[0]; + // Call submodule's setBidderConfig + const newBidderConfig = updateBidderConfig(bidder, ortb2Updates, config.getBidderConfig()); + // Check that the targeting data has been set in the config + expect(newBidderConfig).not.to.be.null; + expect(newBidderConfig.ortb2.site.content.data).to.deep.include(ortb2Updates.siteContentData); + expect(newBidderConfig.ortb2.site.content.data).to.deep.include(previousSiteContent); + }) + }) + + describe('setTargetingDataToConfig', () => { + const expectedKeywords = fakeResponse.t.join(','); + const expectedSiteContentObj = { + data: [{ + name: '1plusX.com', + segment: fakeResponse.t.map((topicId) => ({ id: topicId })), + ext: { segtax: segtaxes.CONTENT } + }] + } + const expectedUserObj = { + data: [{ + name: '1plusX.com', + segment: fakeResponse.s.map((segmentId) => ({ id: segmentId })) + }] + } + const expectedOrtb2 = { + appnexus: { + site: { keywords: expectedKeywords } + }, + rubicon: { + site: { content: expectedSiteContentObj }, + user: expectedUserObj + } + } + + it('sets the config for the selected bidders', () => { + const bidders = ['appnexus', 'rubicon']; + // setting initial config for those bidders + config.setBidderConfig({ + bidders, + config: bidderConfigInitial + }) + // call setTargetingDataToConfig + setTargetingDataToConfig(fakeResponse, { bidders }); + + // Check that the targeting data has been set in both configs + for (const bidder of bidders) { + const newConfig = config.getBidderConfig()[bidder]; + // Check that we got what we expect + const expectedConfErr = (prop) => `New config for ${bidder} doesn't comply with expected at ${prop}`; + expect(newConfig.ortb2.site, expectedConfErr('site')).to.deep.include(expectedOrtb2[bidder].site); + if (expectedOrtb2[bidder].user) { + expect(newConfig.ortb2.user, expectedConfErr('user')).to.deep.include(expectedOrtb2[bidder].user); + } + // Check that existing config didn't get erased + const existingConfErr = (prop) => `Existing config for ${bidder} got unlawfully overwritten at ${prop}`; + expect(newConfig.ortb2.site, existingConfErr('site')).to.deep.include(bidderConfigInitial.ortb2.site); + expect(newConfig.ortb2.user, existingConfErr('user')).to.deep.include(bidderConfigInitial.ortb2.user); + } + }) + }) +})