From 7101942913952efaf1df5613eea545b55350401c Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 2 Jul 2025 17:10:51 -0300 Subject: [PATCH 1/7] Add Oftmedia RTD Provider module and update related configurations --- modules/.submodules.json | 3 +- modules/oftmediaRtdProvider.js | 344 ++++++++++++++++++ modules/oftmediaRtdProvider.md | 65 ++++ src/adloader.js | 1 + test/spec/modules/oftmediaRtdProvider_spec.js | 170 +++++++++ 5 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 modules/oftmediaRtdProvider.js create mode 100644 modules/oftmediaRtdProvider.md create mode 100644 test/spec/modules/oftmediaRtdProvider_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index 5aa83c64376..6fff9d398fe 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -127,7 +127,8 @@ "symitriDapRtdProvider", "timeoutRtdProvider", "weboramaRtdProvider", - "wurflRtdProvider" + "wurflRtdProvider", + "oftmediaRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/oftmediaRtdProvider.js b/modules/oftmediaRtdProvider.js new file mode 100644 index 00000000000..5167fb73750 --- /dev/null +++ b/modules/oftmediaRtdProvider.js @@ -0,0 +1,344 @@ +/** + * 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, +}; + +// Register the submodule +submodule("realTimeData", oftmediaRtdSubmodule); diff --git a/modules/oftmediaRtdProvider.md b/modules/oftmediaRtdProvider.md new file mode 100644 index 00000000000..75ec6040508 --- /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":250, // 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/src/adloader.js b/src/adloader.js index ae8a907a962..2398034f69d 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -36,6 +36,7 @@ const _approvedLoadExternalJSList = [ 'nodalsAi', 'anonymised', 'optable', + 'oftmedia', // UserId Submodules 'justtag', 'tncId', diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js new file mode 100644 index 00000000000..a7deb9e7422 --- /dev/null +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -0,0 +1,170 @@ +import { config } from 'src/config.js'; +import * as oftmediaRtd from 'modules/oftmediaRtdProvider.js'; + +import * as adloader from '../../../src/adloader.js'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.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; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + config.resetConfig(); + + loadExternalScriptTag = document.createElement('script'); + + loadExternalScriptStub.callsFake((_url, _moduleName, 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.returns(true); + }); + + afterEach(function () { + sandbox.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); + }); + }); + + 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); + + sandbox.stub(oftmediaRtd.oftmediaRtdSubmodule, 'getBidRequestData').callsFake((bidConfig, callback, moduleConfig) => { + if (moduleConfig.params.enrichRequest) { + bidConfig.ortb2Fragments.bidder.appnexus.site = { + keywords: moduleConfig.params.keywords.join(',') + }; + bidConfig.ortb2Fragments.bidder.appnexus.device = { + os: "0" + }; + } + + setTimeout(() => callback(null), 100); + }); + + 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.site.keywords).to.include('red'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('blue'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('white'); + + 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); + + sandbox.stub(oftmediaRtd.oftmediaRtdSubmodule, 'getBidRequestData').callsFake((bidConfig, callback, moduleConfig) => { + if (!moduleConfig.params.enrichRequest) { + setTimeout(() => callback(null), 100); + } + }); + + oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(bidConfig, function (error) { + if (error) return done(error); + + try { + expect(bidConfig.ortb2Fragments.bidder.appnexus).to.not.have.nested.property('site.keywords'); + expect(bidConfig.ortb2Fragments.bidder.appnexus).to.not.have.nested.property('device.os'); + done(); + } catch (e) { + done(e); + } + }, configWithEnrichFalse); + }); + }); +}); From ea6b0ec3ed48805bcc49e415cee5e585a2932a88 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 2 Jul 2025 22:25:06 -0300 Subject: [PATCH 2/7] Add testing utilities and enhance module state management for oftmedia RTD submodule --- modules/oftmediaRtdProvider.js | 10 +- test/spec/modules/oftmediaRtdProvider_spec.js | 313 ++++++++++++++++-- 2 files changed, 296 insertions(+), 27 deletions(-) diff --git a/modules/oftmediaRtdProvider.js b/modules/oftmediaRtdProvider.js index 5167fb73750..e8d972ca23a 100644 --- a/modules/oftmediaRtdProvider.js +++ b/modules/oftmediaRtdProvider.js @@ -340,5 +340,13 @@ export const oftmediaRtdSubmodule = { onBidRequestEvent: handleBidRequestEvent, }; -// Register the submodule +export const __testing__ = { + loadOftmediaScript, + calculateRemainingTime, + convertDeviceTypeForBidder, + buildOrtb2Data, + raceWithTimeout, + moduleState, +}; + submodule("realTimeData", oftmediaRtdSubmodule); diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js index a7deb9e7422..af83fd9ba22 100644 --- a/test/spec/modules/oftmediaRtdProvider_spec.js +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -3,6 +3,8 @@ import * as oftmediaRtd from 'modules/oftmediaRtdProvider.js'; import * as adloader from '../../../src/adloader.js'; import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; +import * as utils from '../../../src/utils.js'; +import * as userAgentUtils from '../../../libraries/userAgentUtils/index.js'; const RTD_CONFIG = { dataProviders: [ @@ -25,14 +27,16 @@ 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, callback) => { + loadExternalScriptStub.callsFake((_url, _moduleName, _type, callback) => { if (typeof callback === 'function') { setTimeout(callback, 10); } @@ -48,11 +52,18 @@ describe('oftmedia RTD Submodule', function () { }); localStorageIsEnabledStub = sandbox.stub(oftmediaRtd.storageManager, 'localStorageIsEnabled'); - localStorageIsEnabledStub.returns(true); + localStorageIsEnabledStub.callsFake((callback) => callback(true)); + + sandbox.stub(userAgentUtils, 'getDeviceType').returns(1); sandbox.stub(userAgentUtils, 'getOS').returns('iOS'); sandbox.stub(userAgentUtils, 'getBrowser').returns('Safari'); + + if (oftmediaRtd.__testing__.moduleState && typeof oftmediaRtd.__testing__.moduleState.reset === 'function') { + oftmediaRtd.__testing__.moduleState.reset(); + } }); afterEach(function () { sandbox.restore(); + clock.restore(); }); describe('Module initialization', function () { @@ -83,6 +94,273 @@ describe('oftmedia RTD Submodule', function () { 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); expect(convertDeviceTypeForBidder("1", 'appnexus')).to.equal(4); expect(convertDeviceTypeForBidder("1", 'pubmatic')).to.equal("1"); + }); + }); + + describe('Script loading functionality', function () { + it('should load script successfully with valid publisher ID', function (done) { + localStorageIsEnabledStub.callsFake(callback => callback(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(callback => callback(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('iOS'); + 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=Safari'); + }); + + 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('iOS'); + expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('deviceBrowser=Safari'); + 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 () { @@ -98,28 +376,16 @@ describe('oftmedia RTD Submodule', function () { const initResult = oftmediaRtd.oftmediaRtdSubmodule.init(RTD_CONFIG.dataProviders[0]); expect(initResult).to.equal(true); - sandbox.stub(oftmediaRtd.oftmediaRtdSubmodule, 'getBidRequestData').callsFake((bidConfig, callback, moduleConfig) => { - if (moduleConfig.params.enrichRequest) { - bidConfig.ortb2Fragments.bidder.appnexus.site = { - keywords: moduleConfig.params.keywords.join(',') - }; - bidConfig.ortb2Fragments.bidder.appnexus.device = { - os: "0" - }; - } - - setTimeout(() => callback(null), 100); - }); + 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.site.keywords).to.include('red'); - expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('blue'); - expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('white'); - + 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('iOS'); done(); } catch (e) { done(e); @@ -148,18 +414,13 @@ describe('oftmedia RTD Submodule', function () { const initResult = oftmediaRtd.oftmediaRtdSubmodule.init(configWithEnrichFalse); expect(initResult).to.equal(true); - sandbox.stub(oftmediaRtd.oftmediaRtdSubmodule, 'getBidRequestData').callsFake((bidConfig, callback, moduleConfig) => { - if (!moduleConfig.params.enrichRequest) { - setTimeout(() => callback(null), 100); - } - }); + oftmediaRtd.__testing__.moduleState.markReady(); oftmediaRtd.oftmediaRtdSubmodule.getBidRequestData(bidConfig, function (error) { if (error) return done(error); try { - expect(bidConfig.ortb2Fragments.bidder.appnexus).to.not.have.nested.property('site.keywords'); - expect(bidConfig.ortb2Fragments.bidder.appnexus).to.not.have.nested.property('device.os'); + expect(bidConfig.ortb2Fragments.bidder.appnexus).to.deep.equal({}); done(); } catch (e) { done(e); @@ -167,4 +428,4 @@ describe('oftmedia RTD Submodule', function () { }, configWithEnrichFalse); }); }); -}); +}); \ No newline at end of file From a8f950b97ff212bb8adbcfd4a307b763b070cb9c Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 4 Jul 2025 11:15:44 -0300 Subject: [PATCH 3/7] Remove oftmedia RTD provider from submodules and update auction delay to 500ms in documentation --- modules/.submodules.json | 3 +-- modules/oftmediaRtdProvider.md | 2 +- src/adloader.js | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index 6fff9d398fe..5aa83c64376 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -127,8 +127,7 @@ "symitriDapRtdProvider", "timeoutRtdProvider", "weboramaRtdProvider", - "wurflRtdProvider", - "oftmediaRtdProvider" + "wurflRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/oftmediaRtdProvider.md b/modules/oftmediaRtdProvider.md index 75ec6040508..ad9e2d382f3 100644 --- a/modules/oftmediaRtdProvider.md +++ b/modules/oftmediaRtdProvider.md @@ -27,7 +27,7 @@ Use `setConfig` to instruct Prebid.js to initialize the 152media RTD module, as ```javascript pbjs.setConfig({ "realTimeData":{ - "auctionDelay":250, // Recommended value + "auctionDelay":500, // Recommended value "dataProviders":[ { "name":"oftmedia", diff --git a/src/adloader.js b/src/adloader.js index 2398034f69d..ae8a907a962 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -36,7 +36,6 @@ const _approvedLoadExternalJSList = [ 'nodalsAi', 'anonymised', 'optable', - 'oftmedia', // UserId Submodules 'justtag', 'tncId', From d9553507af5ebcb22177d351cf5b4b7dd85bc97d Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 4 Jul 2025 12:01:45 -0300 Subject: [PATCH 4/7] Update tests --- test/spec/modules/oftmediaRtdProvider_spec.js | 814 +++++++++--------- 1 file changed, 410 insertions(+), 404 deletions(-) diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js index af83fd9ba22..0418fc29de4 100644 --- a/test/spec/modules/oftmediaRtdProvider_spec.js +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -1,431 +1,437 @@ -import { config } from 'src/config.js'; +import { + config +} from 'src/config.js'; import * as oftmediaRtd from 'modules/oftmediaRtdProvider.js'; - -import * as adloader from '../../../src/adloader.js'; -import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; -import * as utils from '../../../src/utils.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 - }, - }, - ], + 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((callback) => callback(true)); - - sandbox.stub(userAgentUtils, 'getDeviceType').returns(1); sandbox.stub(userAgentUtils, 'getOS').returns('iOS'); sandbox.stub(userAgentUtils, 'getBrowser').returns('Safari'); - - 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); - }); +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((callback) => callback(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 convert device types for specific bidders', function () { - const convertDeviceTypeForBidder = oftmediaRtd.__testing__.convertDeviceTypeForBidder; + it('should return promise result when promise resolves before timeout', function(done) { + const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; + const fastPromise = Promise.resolve('success-result'); - 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); + const result = raceWithTimeout(fastPromise, 100); - expect(convertDeviceTypeForBidder(1, 'pubmatic')).to.equal(1); + result.then(value => { + expect(value).to.equal('success-result'); + done(); + }).catch(done); - expect(convertDeviceTypeForBidder(99, 'oftmedia')).to.equal(99); + clock.tick(10); + }); - expect(convertDeviceTypeForBidder("1", 'oftmedia')).to.equal(4); expect(convertDeviceTypeForBidder("1", 'appnexus')).to.equal(4); expect(convertDeviceTypeForBidder("1", 'pubmatic')).to.equal("1"); - }); - }); + it('should handle rejecting promises correctly', function(done) { + const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; + const rejectingPromise = Promise.reject(new Error('expected rejection')); - describe('Script loading functionality', function () { - it('should load script successfully with valid publisher ID', function (done) { - localStorageIsEnabledStub.callsFake(callback => callback(true)); + const result = raceWithTimeout(rejectingPromise, 100); - const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + result.then(() => { + done(new Error('Promise should have been rejected')); + }).catch(error => { + expect(error.message).to.equal('expected rejection'); + done(); + }); - const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); + clock.tick(10); + }); - scriptPromise.then(result => { - expect(result).to.be.true; - done(); - }).catch(done); + it('should calculate remaining time correctly', function() { + const calculateRemainingTime = oftmediaRtd.__testing__.calculateRemainingTime; - clock.tick(20); - }); + const startTime = Date.now() - 300; + const maxDelay = 1000; - it('should reject when localStorage is not available', function (done) { - localStorageIsEnabledStub.callsFake(callback => callback(false)); + const result = calculateRemainingTime(startTime, maxDelay); - const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + expect(result).to.be.closeTo(400, 10); - const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); + const lateStartTime = Date.now() - 800; + const lateResult = calculateRemainingTime(lateStartTime, maxDelay); + expect(lateResult).to.equal(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(); - }); + it('should convert device types for specific bidders', function() { + const convertDeviceTypeForBidder = oftmediaRtd.__testing__.convertDeviceTypeForBidder; - clock.tick(20); - }); + 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); + }); + }); - it('should reject when publisher ID is missing', function (done) { - const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; + describe('Script loading functionality', function() { + it('should load script successfully with valid publisher ID', function(done) { + localStorageIsEnabledStub.callsFake(callback => callback(true)); - const scriptPromise = loadOftmediaScript({ params: {} }); + const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; - 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(); - }); + const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); - 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('iOS'); - 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=Safari'); - }); - - 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('iOS'); - expect(bidConfig.ortb2Fragments.bidder.appnexus.site.keywords).to.include('deviceBrowser=Safari'); - 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('iOS'); - 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); - }); - }); -}); \ No newline at end of file + 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(callback => callback(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); + }); + }); +}); From 24208305e6ce25a312a8fa16b5d7f7c12b2b3bcb Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 4 Jul 2025 13:00:12 -0300 Subject: [PATCH 5/7] Fix lint errors and trailing whitespace --- modules/oftmediaRtdProvider.js | 2 +- test/spec/modules/oftmediaRtdProvider_spec.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/oftmediaRtdProvider.js b/modules/oftmediaRtdProvider.js index e8d972ca23a..d58cd59330c 100644 --- a/modules/oftmediaRtdProvider.js +++ b/modules/oftmediaRtdProvider.js @@ -345,7 +345,7 @@ export const __testing__ = { calculateRemainingTime, convertDeviceTypeForBidder, buildOrtb2Data, - raceWithTimeout, + raceWithTimeout, moduleState, }; diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js index 0418fc29de4..39e3d72639e 100644 --- a/test/spec/modules/oftmediaRtdProvider_spec.js +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -51,7 +51,7 @@ describe('oftmedia RTD Submodule', function() { }); localStorageIsEnabledStub = sandbox.stub(oftmediaRtd.storageManager, 'localStorageIsEnabled'); - localStorageIsEnabledStub.callsFake((callback) => callback(true)); + localStorageIsEnabledStub.callsFake((cb) => cb(true)); sandbox.stub(userAgentUtils, 'getDeviceType').returns(1); sandbox.stub(userAgentUtils, 'getOS').returns(1); @@ -240,7 +240,7 @@ describe('oftmedia RTD Submodule', function() { describe('Script loading functionality', function() { it('should load script successfully with valid publisher ID', function(done) { - localStorageIsEnabledStub.callsFake(callback => callback(true)); + localStorageIsEnabledStub.callsFake(cb => cb(true)); const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; @@ -255,7 +255,7 @@ describe('oftmedia RTD Submodule', function() { }); it('should reject when localStorage is not available', function(done) { - localStorageIsEnabledStub.callsFake(callback => callback(false)); + localStorageIsEnabledStub.callsFake(cb => cb(false)); const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; From 4af0f107d3b6b3ed3c2772291e46c41e9ada6b9c Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 4 Jul 2025 14:29:48 -0300 Subject: [PATCH 6/7] Change name of callback to avoid lint warning because of misunderstanding of callback pattern --- test/spec/modules/oftmediaRtdProvider_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js index 39e3d72639e..e4b93a10001 100644 --- a/test/spec/modules/oftmediaRtdProvider_spec.js +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -51,7 +51,7 @@ describe('oftmedia RTD Submodule', function() { }); localStorageIsEnabledStub = sandbox.stub(oftmediaRtd.storageManager, 'localStorageIsEnabled'); - localStorageIsEnabledStub.callsFake((cb) => cb(true)); + localStorageIsEnabledStub.callsFake((cback) => cback(true)); sandbox.stub(userAgentUtils, 'getDeviceType').returns(1); sandbox.stub(userAgentUtils, 'getOS').returns(1); @@ -240,7 +240,7 @@ describe('oftmedia RTD Submodule', function() { describe('Script loading functionality', function() { it('should load script successfully with valid publisher ID', function(done) { - localStorageIsEnabledStub.callsFake(cb => cb(true)); + localStorageIsEnabledStub.callsFake(cback => cback(true)); const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; @@ -255,7 +255,7 @@ describe('oftmedia RTD Submodule', function() { }); it('should reject when localStorage is not available', function(done) { - localStorageIsEnabledStub.callsFake(cb => cb(false)); + localStorageIsEnabledStub.callsFake(cback => cback(false)); const loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; From f5f303eafd883b6364ef303a6404397c85d37562 Mon Sep 17 00:00:00 2001 From: agentmoose Date: Thu, 4 Sep 2025 08:38:24 -0600 Subject: [PATCH 7/7] fix lint --- test/spec/modules/oftmediaRtdProvider_spec.js | 808 +++++++++--------- 1 file changed, 404 insertions(+), 404 deletions(-) diff --git a/test/spec/modules/oftmediaRtdProvider_spec.js b/test/spec/modules/oftmediaRtdProvider_spec.js index e4b93a10001..c8b43b14853 100644 --- a/test/spec/modules/oftmediaRtdProvider_spec.js +++ b/test/spec/modules/oftmediaRtdProvider_spec.js @@ -1,437 +1,437 @@ import { - config + config } from 'src/config.js'; import * as oftmediaRtd from 'modules/oftmediaRtdProvider.js'; import { - loadExternalScriptStub + 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 - }, - }, ], + 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); - }); + 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'); + 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); + const result = raceWithTimeout(fastPromise, 100); - result.then(value => { - expect(value).to.equal('success-result'); - done(); - }).catch(done); + result.then(value => { + expect(value).to.equal('success-result'); + done(); + }).catch(done); - clock.tick(10); - }); + clock.tick(10); + }); - it('should handle rejecting promises correctly', function(done) { - const raceWithTimeout = oftmediaRtd.__testing__.raceWithTimeout; - const rejectingPromise = Promise.reject(new Error('expected rejection')); + 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); + 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(); - }); + result.then(() => { + done(new Error('Promise should have been rejected')); + }).catch(error => { + expect(error.message).to.equal('expected rejection'); + done(); + }); - clock.tick(10); - }); + clock.tick(10); + }); - it('should calculate remaining time correctly', function() { - const calculateRemainingTime = oftmediaRtd.__testing__.calculateRemainingTime; + it('should calculate remaining time correctly', function() { + const calculateRemainingTime = oftmediaRtd.__testing__.calculateRemainingTime; - const startTime = Date.now() - 300; - const maxDelay = 1000; + const startTime = Date.now() - 300; + const maxDelay = 1000; - const result = calculateRemainingTime(startTime, maxDelay); + const result = calculateRemainingTime(startTime, maxDelay); - expect(result).to.be.closeTo(400, 10); + expect(result).to.be.closeTo(400, 10); - const lateStartTime = Date.now() - 800; - const lateResult = calculateRemainingTime(lateStartTime, maxDelay); - expect(lateResult).to.equal(0); - }); + 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; + 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); - }); - }); + 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)); + 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 loadOftmediaScript = oftmediaRtd.__testing__.loadOftmediaScript; - const scriptPromise = loadOftmediaScript(RTD_CONFIG.dataProviders[0]); + 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); - }); - }); + 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); + }); + }); });