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);
+ }
+ })
+ })
+})