diff --git a/modules/oftmediaRtdProvider.js b/modules/oftmediaRtdProvider.js new file mode 100644 index 00000000000..d58cd59330c --- /dev/null +++ b/modules/oftmediaRtdProvider.js @@ -0,0 +1,352 @@ +/** + * Oftmedia Real-Time Data (RTD) Provider Module + * + * This module enriches bid requests with device type, OS, and browser information + * for improved ad targeting capabilities. + */ + +import { MODULE_TYPE_RTD } from "../src/activities/modules.js"; +import { loadExternalScript } from "../src/adloader.js"; +import { submodule } from "../src/hook.js"; +import { config as prebidConfig } from "../src/config.js"; +import { getStorageManager } from "../src/storageManager.js"; +import { prefixLog, mergeDeep, isStr } from "../src/utils.js"; +import { + getDeviceType, + getOS, + getBrowser, +} from "../libraries/userAgentUtils/index.js"; + +// Module constants +const MODULE_NAME = "oftmedia"; +const EXTERNAL_SCRIPT_URL = "https://bidlift.152media.info/rtd"; +const DEFAULT_TIMEOUT = 1500; +const TIMEOUT_BUFFER_RATIO = 0.7; + +// Device type mappings for ORTB2 compliance +const DEVICE_TYPE_ORTB2_MAP = { + 0: 2, // Unknown -> PC + 1: 4, // Mobile -> Phone + 2: 5, // Tablet -> Tablet +}; + +// Module setup +export const storageManager = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, +}); + +const { logError, logWarn, logInfo } = prefixLog(`${MODULE_NAME}RtdProvider:`); + +/** + * Module state management + */ +class ModuleState { + constructor() { + this.initTimestamp = null; + this.scriptLoadPromise = null; + this.isReady = false; + this.readyCallbacks = []; + } + + markReady() { + this.isReady = true; + this.readyCallbacks.forEach((callback) => callback()); + this.readyCallbacks = []; + } + + onReady(callback) { + if (this.isReady) { + callback(); + } else { + this.readyCallbacks.push(callback); + } + } + + reset() { + this.initTimestamp = null; + this.scriptLoadPromise = null; + this.isReady = false; + this.readyCallbacks = []; + } +} + +const moduleState = new ModuleState(); + +/** + * Creates a promise that resolves after specified timeout + * @param {number} timeoutMs - Timeout in milliseconds + * @returns {Promise} Promise that resolves to undefined after timeout + */ +function createTimeoutPromise(timeoutMs) { + return new Promise((resolve) => { + setTimeout(() => resolve(undefined), timeoutMs); + }); +} + +/** + * Races a promise against a timeout + * @param {Promise} promise - Promise to race + * @param {number} timeoutMs - Timeout in milliseconds + * @returns {Promise} Resolves with promise result or undefined on timeout + */ +function raceWithTimeout(promise, timeoutMs) { + const timeoutPromise = createTimeoutPromise(timeoutMs); + return Promise.race([promise, timeoutPromise]); +} + +/** + * Calculates remaining time based on auction delay and elapsed time + * @param {number} startTime - Start timestamp + * @param {number} maxDelay - Maximum allowed delay + * @returns {number} Remaining time in milliseconds + */ +function calculateRemainingTime(startTime, maxDelay) { + const elapsed = Date.now() - startTime; + const allowedTime = maxDelay * TIMEOUT_BUFFER_RATIO; + return Math.max(0, allowedTime - elapsed); +} + +/** + * Loads external Oftmedia script + * @param {Object} moduleConfig - Configuration object + * @returns {Promise} Promise resolving to true on success + */ +function loadOftmediaScript(moduleConfig) { + const publisherId = moduleConfig?.params?.publisherId; + + if (!publisherId) { + const error = new Error("Publisher ID is required for script loading"); + logError(error.message); + return Promise.reject(error); + } + + return new Promise((resolve, reject) => { + // Check localStorage availability + storageManager.localStorageIsEnabled((hasStorage) => { + if (!hasStorage) { + const error = new Error("localStorage is not available"); + logWarn(error.message + ", skipping script load"); + return reject(error); + } + + const scriptUrl = `${EXTERNAL_SCRIPT_URL}?pub_id=${publisherId}`; + const onLoadSuccess = () => { + logInfo("External script loaded successfully"); + resolve(true); + }; + + try { + loadExternalScript( + scriptUrl, + MODULE_TYPE_RTD, + MODULE_NAME, + onLoadSuccess, + undefined, + { pub_id: publisherId } + ); + } catch (error) { + logError("Failed to load external script:", error); + reject(error); + } + }); + }); +} + +/** + * Converts device type to ORTB2 format for specific bidders + * @param {number} deviceType - Original device type + * @param {string} bidderCode - Bidder identifier + * @returns {number} Converted device type + */ +function convertDeviceTypeForBidder(deviceType, bidderCode) { + const convertibleBidders = ["oftmedia", "appnexus"]; + + if (!convertibleBidders.includes(bidderCode)) { + return deviceType; + } + + const convertedType = DEVICE_TYPE_ORTB2_MAP[deviceType]; + if (convertedType === undefined) { + logWarn( + `No ORTB2 mapping found for device type ${deviceType}, using original` + ); + return deviceType; + } + + return convertedType; +} + +/** + * Builds ORTB2 data object for bid enrichment + * @param {Object} config - Module configuration + * @returns {Object|null} ORTB2 data object or null if invalid + */ +function buildOrtb2Data(config) { + const deviceType = getDeviceType(); + const deviceOS = getOS(); + const browserType = getBrowser(); + const bidderCode = config?.params?.bidderCode; + const enrichRequest = config?.params?.enrichRequest; + + const configuredKeywords = config?.params?.keywords || []; + + if (!enrichRequest) { + logWarn("Enrich request is not enabled, skipping ORTB2 data build"); + return null; + } + + if (!bidderCode) { + logError("Bidder code is required in configuration"); + return null; + } + + // Convert device type if needed + const finalDeviceType = convertDeviceTypeForBidder(deviceType, bidderCode); + + // Build keywords array + const allKeywords = [...configuredKeywords, `deviceBrowser=${browserType}`]; + + return { + bidderCode, + ortb2Data: { + device: { + devicetype: finalDeviceType, + os: deviceOS.toString(), + }, + site: { + keywords: allKeywords.join(", "), + }, + }, + }; +} + +/** + * Initialize the RTD module + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent object (unused) + * @returns {boolean} True if initialization started successfully + */ +function initializeModule(config, userConsent) { + moduleState.reset(); + moduleState.initTimestamp = Date.now(); + + // Validate publisher ID + if (!isStr(config?.params?.publisherId)) { + logError("Publisher ID must be provided as a string"); + return false; + } + + // Start script loading process + moduleState.scriptLoadPromise = loadOftmediaScript(config); + + // Handle script loading completion + moduleState.scriptLoadPromise + .then(async () => { + const auctionDelay = + prebidConfig.getConfig("realTimeData")?.auctionDelay || DEFAULT_TIMEOUT; + const remainingTime = calculateRemainingTime( + moduleState.initTimestamp, + auctionDelay + ); + + // Wait for script with remaining time budget + const result = await raceWithTimeout( + moduleState.scriptLoadPromise, + remainingTime + ); + + if (result) { + logInfo("Script loaded within time budget"); + } else { + logWarn("Script loading exceeded time budget"); + } + + moduleState.markReady(); + }) + .catch((error) => { + logError("Script loading failed:", error); + moduleState.markReady(); + }); + + return true; +} + +/** + * Process bid request data and add RTD enrichment + * @param {Object} bidRequestConfig - Bid request configuration object + * @param {Function} done - Callback function to signal completion + * @param {Object} config - Module configuration + */ +function processBidRequestData(bidRequestConfig, done, config) { + // Wait for module to be ready + moduleState.onReady(() => { + try { + // Validate bid request structure + if (!bidRequestConfig?.ortb2Fragments?.bidder) { + logError( + "Invalid bid request structure: missing ortb2Fragments.bidder" + ); + return done(); + } + + if (config?.params?.enrichRequest === true) { + // Build enrichment data + const enrichmentData = buildOrtb2Data(config); + + logInfo("Building ORTB2 enrichment data", enrichmentData); + + if (!enrichmentData) { + logInfo("Could not build ORTB2 enrichment data"); + return done(); + } + + // Apply enrichment to bid request + mergeDeep(bidRequestConfig.ortb2Fragments.bidder, { + [enrichmentData.bidderCode]: enrichmentData.ortb2Data, + }); + + logInfo("Bid request enriched successfully"); + } + done(); + } catch (error) { + logError("Error processing bid request data:", error); + done(); + } + }); +} + +/** + * Handle bid request events (for debugging/monitoring) + * @param {Object} bidderRequest - Bidder request object + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent object + */ +function handleBidRequestEvent(bidderRequest, config, userConsent) { + logInfo("Bid request event received", { + bidderRequest: JSON.stringify(bidderRequest), + config, + userConsent, + }); +} + +/** + * RTD Submodule definition + */ +export const oftmediaRtdSubmodule = { + name: MODULE_NAME, + init: initializeModule, + getBidRequestData: processBidRequestData, + onBidRequestEvent: handleBidRequestEvent, +}; + +export const __testing__ = { + loadOftmediaScript, + calculateRemainingTime, + convertDeviceTypeForBidder, + buildOrtb2Data, + raceWithTimeout, + moduleState, +}; + +submodule("realTimeData", oftmediaRtdSubmodule); diff --git a/modules/oftmediaRtdProvider.md b/modules/oftmediaRtdProvider.md new file mode 100644 index 00000000000..ad9e2d382f3 --- /dev/null +++ b/modules/oftmediaRtdProvider.md @@ -0,0 +1,65 @@ +# 152media (Oftmedia) Real-time Data Submodule + +## Overview + + Module Name: 152media (Oftmedia) RTD Provider + Module Type: RTD Provider + Maintainer: hello@152media.com + +## Description + +The 152media RTD module enhances programmatic advertising by providing real-time contextual data and audience insights. Publishers can use this module to augment ad requests with relevant targeting parameters, optimize revenue through improved ad relevance, and filter out requests that are inefficient or provide no value to buyers. The module leverages AI models to generate optimized deals and augment targeting signals for enhanced bid performance. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,oftmediaRtdProvider" +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the 152media RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the 152media RTD module, as specified below. + + +```javascript +pbjs.setConfig({ + "realTimeData":{ + "auctionDelay":500, // Recommended value + "dataProviders":[ + { + "name":"oftmedia", + "waitForIt":true, // Recommended value + "params":{ + "publisherId": "0653b3fc-a645-4bcc-bfee-b8982974dd53", // The Publisher ID is provided by 152Media. For assistance, contact hello@152media.com. + "keywords":[ // Keywords provided by 152Media + "red", + "blue", + "white" + ], + "bidderCode": "appnexus", // Define the bidder code to enable optimization. + "enrichRequest": true // Optional: Set to true to enrich the request with additional targeting data. + } + } + ] + } +}); +``` + +### Parameters + +| Name | Type | Description | Default | +| :------------------------ | :------------ | :--------------------------------------------------------------- |:----------------- | +| name | String | Real time data module name | Always 'oftmedia' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params.publisherId | String | Your 152media publisher ID | | +| params.keywords | Array | List of contextual keywords for targeting enhancement | [] | +| params.bidderCode | String | Primary bidder code for optimization | +| params.timeout | Integer | Request timeout in milliseconds | 1000ms | +| params.enrichRequest | Boolean | Set to `true` to enrich the request with data | `false` | + +## Support + +If you have any questions or need assistance with implementation, please reach out to us at hello@152media.com. diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js new file mode 100644 index 00000000000..c8b43b14853 --- /dev/null +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -0,0 +1,437 @@ +import { + config +} from 'src/config.js'; +import * as oftmediaRtd from 'modules/oftmediaRtdProvider.js'; +import { + loadExternalScriptStub +} from 'test/mocks/adloaderStub.js'; +import * as userAgentUtils from '../../../libraries/userAgentUtils/index.js'; + +const RTD_CONFIG = { + dataProviders: [{ + name: 'oftmedia', + waitForIt: true, + params: { + publisherId: '0653b3fc-a645-4bcc-bfee-b8982974dd53', + keywords: ['red', 'blue', 'white'], + bidderCode: 'appnexus', + enrichRequest: true + }, + }, ], +}; + +const TIMEOUT = 10; + +describe('oftmedia RTD Submodule', function() { + let sandbox; + let loadExternalScriptTag; + let localStorageIsEnabledStub; + let clock; + + beforeEach(function() { + sandbox = sinon.createSandbox(); + clock = sandbox.useFakeTimers(); + config.resetConfig(); + + loadExternalScriptTag = document.createElement('script'); + + loadExternalScriptStub.callsFake((_url, _moduleName, _type, callback) => { + if (typeof callback === 'function') { + setTimeout(callback, 10); + } + + setTimeout(() => { + if (loadExternalScriptTag.onload) { + loadExternalScriptTag.onload(); + } + loadExternalScriptTag.dispatchEvent(new Event('load')); + }, 10); + + return loadExternalScriptTag; + }); + + localStorageIsEnabledStub = sandbox.stub(oftmediaRtd.storageManager, 'localStorageIsEnabled'); + localStorageIsEnabledStub.callsFake((cback) => cback(true)); + + sandbox.stub(userAgentUtils, 'getDeviceType').returns(1); + sandbox.stub(userAgentUtils, 'getOS').returns(1); + sandbox.stub(userAgentUtils, 'getBrowser').returns(2); + + if (oftmediaRtd.__testing__.moduleState && typeof oftmediaRtd.__testing__.moduleState.reset === 'function') { + oftmediaRtd.__testing__.moduleState.reset(); + } + }); + + afterEach(function() { + sandbox.restore(); + clock.restore(); + }); + + describe('Module initialization', function() { + it('should initialize and return true when publisherId is provided', function() { + const result = oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + expect(result).to.equal(true); + }); + + it('should return false when publisherId is not provided', function() { + const invalidConfig = { + params: { + bidderCode: 'appnexus', + enrichRequest: true + } + }; + const result = oftmediaRtd.oftmediaRtdSubmodule.init(invalidConfig); + expect(result).to.equal(false); + }); + + it('should return false when publisherId is not a string', function() { + const invalidConfig = { + params: { + publisherId: 12345, + bidderCode: 'appnexus', + enrichRequest: true + } + }; + const result = oftmediaRtd.oftmediaRtdSubmodule.init(invalidConfig); + expect(result).to.equal(false); + }); + + it('should set initTimestamp when initialized', function() { + const result = oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + expect(result).to.equal(true); + expect(oftmediaRtd.__testing__.moduleState.initTimestamp).to.be.a('number'); + }); + }); + + describe('ModuleState class functionality', function() { + it('should mark module as ready and execute callbacks', function() { + const moduleState = oftmediaRtd.__testing__.moduleState; + const callbackSpy = sinon.spy(); + + moduleState.onReady(callbackSpy); + expect(callbackSpy.called).to.be.false; + + moduleState.markReady(); + expect(callbackSpy.calledOnce).to.be.true; + + const secondCallbackSpy = sinon.spy(); + moduleState.onReady(secondCallbackSpy); + expect(secondCallbackSpy.calledOnce).to.be.true; + }); + + it('should reset module state properly', function() { + const moduleState = oftmediaRtd.__testing__.moduleState; + + moduleState.initTimestamp = 123; + moduleState.scriptLoadPromise = Promise.resolve(); + moduleState.isReady = true; + + moduleState.reset(); + + expect(moduleState.initTimestamp).to.be.null; + expect(moduleState.scriptLoadPromise).to.be.null; + expect(moduleState.isReady).to.be.false; + expect(moduleState.readyCallbacks).to.be.an('array').that.is.empty; + }); + }); + + describe('Helper functions', function() { + it('should create a timeout promise that resolves after specified time', function(done) { + let promiseResolved = false; + + oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + + const testPromise = new Promise(resolve => { + setTimeout(() => { + promiseResolved = true; + resolve('test-result'); + }, 100); + }); + + const racePromise = oftmediaRtd.__testing__.raceWithTimeout(testPromise, 50); + + racePromise.then(result => { + expect(promiseResolved).to.be.false; + expect(result).to.be.undefined; + done(); + }).catch(done); + + clock.tick(60); + }); + + it('should create a timeout promise that resolves with undefined after specified time', function(done) { + const neverResolvingPromise = new Promise(() => {}); + + const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; + const result = raceWithTimeout(neverResolvingPromise, 200); + + let resolved = false; + + result.then(value => { + resolved = true; + expect(value).to.be.undefined; + done(); + }).catch(done); + + expect(resolved).to.be.false; + clock.tick(100); + expect(resolved).to.be.false; + + clock.tick(150); + }); + + it('should return promise result when promise resolves before timeout', function(done) { + const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; + const fastPromise = Promise.resolve('success-result'); + + const result = raceWithTimeout(fastPromise, 100); + + result.then(value => { + expect(value).to.equal('success-result'); + done(); + }).catch(done); + + clock.tick(10); + }); + + it('should handle rejecting promises correctly', function(done) { + const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; + const rejectingPromise = Promise.reject(new Error('expected rejection')); + + const result = raceWithTimeout(rejectingPromise, 100); + + result.then(() => { + done(new Error('Promise should have been rejected')); + }).catch(error => { + expect(error.message).to.equal('expected rejection'); + done(); + }); + + clock.tick(10); + }); + + it('should calculate remaining time correctly', function() { + const calculateRemainingTime = oftmediaRtd.__testing__.calculateRemainingTime; + + const startTime = Date.now() - 300; + const maxDelay = 1000; + + const result = calculateRemainingTime(startTime, maxDelay); + + expect(result).to.be.closeTo(400, 10); + + const lateStartTime = Date.now() - 800; + const lateResult = calculateRemainingTime(lateStartTime, maxDelay); + expect(lateResult).to.equal(0); + }); + + it('should convert device types for specific bidders', function() { + const convertDeviceTypeForBidder = oftmediaRtd.__testing__.convertDeviceTypeForBidder; + + expect(convertDeviceTypeForBidder(0, 'oftmedia')).to.equal(2); + expect(convertDeviceTypeForBidder(1, 'oftmedia')).to.equal(4); + expect(convertDeviceTypeForBidder(2, 'oftmedia')).to.equal(5); + expect(convertDeviceTypeForBidder(1, 'appnexus')).to.equal(4); + expect(convertDeviceTypeForBidder(1, 'pubmatic')).to.equal(1); + expect(convertDeviceTypeForBidder(99, 'oftmedia')).to.equal(99); + expect(convertDeviceTypeForBidder("1", 'oftmedia')).to.equal(4); + }); + }); + + describe('Script loading functionality', function() { + it('should load script successfully with valid publisher ID', function(done) { + localStorageIsEnabledStub.callsFake(cback => cback(true)); + + const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + + const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); + + scriptPromise.then(result => { + expect(result).to.be.true; + done(); + }).catch(done); + + clock.tick(20); + }); + + it('should reject when localStorage is not available', function(done) { + localStorageIsEnabledStub.callsFake(cback => cback(false)); + + const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + + const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); + + scriptPromise.then(() => { + done(new Error('Promise should be rejected when localStorage is not available')); + }).catch(error => { + expect(error.message).to.include('localStorage is not available'); + done(); + }); + + clock.tick(20); + }); + + it('should reject when publisher ID is missing', function(done) { + const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + + const scriptPromise = loadOftmediaScript({ + params: {} + }); + + scriptPromise.then(() => { + done(new Error('Promise should be rejected when publisher ID is missing')); + }).catch(error => { + expect(error.message).to.include('Publisher ID is required'); + done(); + }); + + clock.tick(20); + }); + }); + + describe('ORTB2 data building', function() { + it('should build valid ORTB2 data object with device and keywords', function() { + const buildOrtb2Data = oftmediaRtd.__testing__.buildOrtb2Data; + + const result = buildOrtb2Data(RTD_CONFIG.dataProviders[0]); + + expect(result).to.be.an('object'); + expect(result.bidderCode).to.equal('appnexus'); + expect(result.ortb2Data).to.be.an('object'); + expect(result.ortb2Data.device.devicetype).to.equal(4); + expect(result.ortb2Data.device.os).to.equal('1'); + expect(result.ortb2Data.site.keywords).to.include('red'); + expect(result.ortb2Data.site.keywords).to.include('blue'); + expect(result.ortb2Data.site.keywords).to.include('white'); + expect(result.ortb2Data.site.keywords).to.include('deviceBrowser=2'); + }); + + it('should return null when enrichRequest is false', function() { + const buildOrtb2Data = oftmediaRtd.__testing__.buildOrtb2Data; + + const result = buildOrtb2Data({ + params: { + publisherId: '0653b3fc-a645-4bcc-bfee-b8982974dd53', + bidderCode: 'appnexus', + enrichRequest: false + } + }); + + expect(result).to.be.null; + }); + + it('should return null when bidderCode is missing', function() { + const buildOrtb2Data = oftmediaRtd.__testing__.buildOrtb2Data; + + const result = buildOrtb2Data({ + params: { + publisherId: '0653b3fc-a645-4bcc-bfee-b8982974dd53', + enrichRequest: true + } + }); + + expect(result).to.be.null; + }); + }); + + describe('Bid request processing', function() { + it('should process bid request and enrich it with ORTB2 data', function(done) { + oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + + oftmediaRtd.__testing__.moduleState.markReady(); + + const bidConfig = { + ortb2Fragments: { + bidder: { + appnexus: {} + } + } + }; + + oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(bidConfig, () => { + expect(bidConfig.ortb2Fragments.bidder.appnexus.device).to.be.an('object'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.device.devicetype).to.equal(4); + expect(bidConfig.ortb2Fragments.bidder.appnexus.device.os).to.equal('1'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('deviceBrowser=2'); + done(); + }, RTD_CONFIG.dataProviders[0]); + }); + + it('should handle invalid bid request structure', function(done) { + oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + oftmediaRtd.__testing__.moduleState.markReady(); + const invalidBidConfig = {}; + + oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(invalidBidConfig, () => { + expect(invalidBidConfig.ortb2Fragments).to.be.undefined; + done(); + }, RTD_CONFIG.dataProviders[0]); + }); + }); + + describe('Bid request enrichment', function() { + it('should enrich bid request with keywords, OS and device when enrichRequest is true', function(done) { + const bidConfig = { + ortb2Fragments: { + bidder: { + appnexus: {} + } + } + }; + + const initResult = oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); + expect(initResult).to.equal(true); + + oftmediaRtd.__testing__.moduleState.markReady(); + + oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(bidConfig, function(error) { + if (error) return done(error); + + try { + expect(bidConfig.ortb2Fragments.bidder.appnexus).to.have.nested.property('site.keywords'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.device).to.be.an('object'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.device.devicetype).to.equal(4); + expect(bidConfig.ortb2Fragments.bidder.appnexus.device.os).to.equal('1'); + done(); + } catch (e) { + done(e); + } + }, RTD_CONFIG.dataProviders[0]); + }); + + it('should not enrich bid request when enrichRequest is false', function(done) { + const configWithEnrichFalse = { + params: { + publisherId: '0653b3fc-a645-4bcc-bfee-b8982974dd53', + keywords: ['red', 'blue', 'white'], + bidderCode: 'appnexus', + enrichRequest: false + } + }; + + const bidConfig = { + ortb2Fragments: { + bidder: { + appnexus: {} + } + } + }; + + const initResult = oftmediaRtd.oftmediaRtdSubmodule.init(configWithEnrichFalse); + expect(initResult).to.equal(true); + + oftmediaRtd.__testing__.moduleState.markReady(); + + oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(bidConfig, function(error) { + if (error) return done(error); + + try { + expect(bidConfig.ortb2Fragments.bidder.appnexus).to.deep.equal({}); + done(); + } catch (e) { + done(e); + } + }, configWithEnrichFalse); + }); + }); +});