diff --git a/libraries/adsmartxUtils/bidderUtils.js b/libraries/adsmartxUtils/bidderUtils.js new file mode 100644 index 00000000000..597403b72e9 --- /dev/null +++ b/libraries/adsmartxUtils/bidderUtils.js @@ -0,0 +1,263 @@ +import { BANNER, VIDEO } from '../../src/mediaTypes.js'; +import { ortbConverter } from '../ortbConverter/converter.js'; +import { deepAccess, logInfo, logWarn } from '../../src/utils.js'; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 60; + +/** + * Get publisher user ID with priority: + * 1. Bid params (sspUserId) + * 2. ORTB2 first party data (ortb2.user.id) + * @param {Object} bidParams - Bid parameters from first bid + * @param {Object} bidderRequest - Bidder request object containing ortb2 + * @returns {string|null} Publisher user ID if found, null otherwise + */ +export function getPublisherUserId(bidParams, bidderRequest) { + if (bidParams?.sspUserId) { + logInfo('Using SSP user ID from bid params:', bidParams.sspUserId); + return bidParams.sspUserId; + } + const ortb2UserId = deepAccess(bidderRequest, 'ortb2.user.id'); + if (ortb2UserId) { + logInfo('Using SSP user ID from ORTB2 user.id:', ortb2UserId); + return ortb2UserId; + } + logInfo('No SSP user ID found in bid params or ORTB2'); + return null; +} + +/** + * Creates ORTB converter with shared imp/request logic. + * @param {Object} config - { defaultCurrency, defaultTtl } + * @returns {Object} ortbConverter instance + */ +export function createConverter(config = {}) { + const currency = config.defaultCurrency ?? DEFAULT_CURRENCY; + const ttl = config.defaultTtl ?? DEFAULT_TTL; + + return ortbConverter({ + context: { + netRevenue: true, + ttl, + currency, + }, + imp(buildImp, bidRequest, context) { + logInfo('Building impression object for bidRequest:', bidRequest); + const imp = buildImp(bidRequest, context); + const { mediaTypes } = bidRequest; + if (bidRequest.params?.bidfloor) { + logInfo('Setting bid floor for impression:', bidRequest.params.bidfloor); + imp.bidfloor = bidRequest.params.bidfloor; + } + if (mediaTypes[BANNER]) { + logInfo('Adding banner media type to impression:', mediaTypes[BANNER]); + imp.banner = { ...(imp.banner || {}), format: mediaTypes[BANNER].sizes.map(([w, h]) => ({ w, h })) }; + } else if (mediaTypes[VIDEO]) { + logInfo('Adding video media type to impression:', mediaTypes[VIDEO]); + imp.video = { ...(imp.video || {}), ...mediaTypes[VIDEO] }; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + logInfo('Building server request with impressions:', imps); + const request = buildRequest(imps, bidderRequest, context); + request.cur = [currency]; + request.tmax = bidderRequest.timeout; + request.test = bidderRequest.test || 0; + + if (Array.isArray(bidderRequest.bids)) { + const hasTestMode = bidderRequest.bids.some(bid => bid.params?.testMode === 1); + if (hasTestMode) { + request.ext = request.ext || {}; + request.ext.test = 1; + logInfo('Test mode detected in bid params, setting test flag in request:', request.ext.test); + } + const sspIdBid = bidderRequest.bids.find(bid => bid.params?.sspId); + if (sspIdBid) { + request.ext = request.ext || {}; + request.ext.sspId = sspIdBid.params.sspId; + logInfo('sspId detected in bid params, setting sspId in request:', request.ext.sspId); + } + const siteIdBid = bidderRequest.bids.find(bid => bid.params?.siteId); + if (siteIdBid) { + request.ext = request.ext || {}; + request.ext.siteId = siteIdBid.params.siteId; + logInfo('siteId detected in bid params, setting siteId in request:', request.ext.siteId); + } + } + + if (bidderRequest.gdprConsent || bidderRequest.uspConsent) { + request.regs = request.regs || {}; + request.user = request.user || {}; + } + if (bidderRequest.gdprConsent) { + logInfo('Adding GDPR consent information to request:', bidderRequest.gdprConsent); + request.regs.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + request.user.consent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.uspConsent) { + logInfo('Adding USP consent information to request:', bidderRequest.uspConsent); + request.regs.ext = request.regs.ext || {}; + request.regs.ext.us_privacy = bidderRequest.uspConsent; + } + return request; + }, + }); +} + +/** + * Validates the bid request (video mimes/sizes, etc.). + * @param {Object} bid - The bid request object. + * @returns {boolean} True if the bid request is valid. + */ +export function isBidRequestValid(bid) { + logInfo('Validating bid request:', bid); + const { mediaTypes } = bid; + + if (mediaTypes?.[VIDEO]) { + const video = mediaTypes[VIDEO]; + if (!video.mimes || !Array.isArray(video.mimes) || video.mimes.length === 0) { + logWarn('Invalid video bid request: Missing or invalid mimes.'); + return false; + } + // w and h are optional; if provided they must be positive + if (video.w != null && video.w <= 0) { + logWarn('Invalid video bid request: Invalid width.'); + return false; + } + if (video.h != null && video.h <= 0) { + logWarn('Invalid video bid request: Invalid height.'); + return false; + } + } + return true; +} + +/** + * Builds buildRequests function that uses the given converter and endpoint. + * @param {Object} config - { converter, endpointUrl } + * @returns {function(Array, Object): Object} + */ +export function createBuildRequests(config) { + const { converter, endpointUrl } = config; + + return function buildRequests(validBidRequests, bidderRequest) { + logInfo('Building server request for valid bid requests:', validBidRequests); + + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + logInfo('Converted to ORTB request:', request); + return { + method: 'POST', + url: endpointUrl, + data: request, + options: { endpointCompression: true }, + }; + }; +} + +/** + * Interprets the server response and extracts bid information. + * @param {Object} serverResponse - The response from the server. + * @param {Object} request - The original request sent to the server. + * @param {Object} config - { defaultCurrency, defaultTtl } + * @returns {Array} Array of bid objects. + */ +export function interpretResponse(serverResponse, request, config = {}) { + const defaultCurrency = config.defaultCurrency ?? DEFAULT_CURRENCY; + const defaultTtl = config.defaultTtl ?? DEFAULT_TTL; + + logInfo('Interpreting server response:', serverResponse); + const bidResp = serverResponse?.body; + if (!bidResp || !Array.isArray(bidResp.seatbid)) { + logWarn('Server response is empty, invalid, or does not contain seatbid array.'); + return []; + } + + const responses = []; + bidResp.seatbid.forEach(seatbid => { + if (!Array.isArray(seatbid.bid) || seatbid.bid.length === 0) return; + const bid = seatbid.bid[0]; + if (!bid.impid || bid.price == null) { + logWarn('Skipping bid with missing impid or price, bidId:', bid.id); + return; + } + logInfo('Processing bid response:', bid); + const bidResponse = { + requestId: bid.impid, + cpm: bid.price, + currency: bidResp.cur || defaultCurrency, + width: bid.w, + height: bid.h, + ad: bid.adm, + creativeId: bid.crid, + netRevenue: true, + ttl: defaultTtl, + meta: { advertiserDomains: bid.adomain || [] }, + }; + + switch (bid.mtype) { + case 1: + bidResponse.mediaType = BANNER; + break; + case 2: + bidResponse.mediaType = VIDEO; + bidResponse.vastXml = bid.adm; + break; + default: + if (bid.mtype != null) { + logWarn('Unknown media type: ', bid.mtype, ' for bidId: ', bid.id); + } else { + logWarn('Bid response does not contain media type for bidId: ', bid.id); + } + bidResponse.mediaType = BANNER; + break; + } + + if (bid.dealid) bidResponse.dealId = bid.dealid; + logInfo('Interpreted response:', bidResponse, ' for bidId: ', bid.id); + responses.push(bidResponse); + }); + + logInfo('Interpreted bid responses:', responses); + return responses; +} + +/** + * Creates getUserSyncs function that builds sync URL with privacy params. + * @param {string} syncUrl - Base sync URL (e.g. 'https://sync.adsmartx.com/sync') + * @returns {function(Object, Array, Object, string, Object): Array} + */ +export function createGetUserSyncs(syncUrl) { + return function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + logInfo('getUserSyncs called with options:', syncOptions); + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + logWarn('User sync disabled: neither iframe nor pixel is enabled'); + return []; + } + + const params = []; + if (gdprConsent) { + params.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + params.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + params.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + params.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } + + params.push('ssp_id=630141'); + params.push('iframe_enabled=' + (syncOptions.iframeEnabled ? 'true' : 'false')); + + const queryString = params.length ? '?' + params.join('&') : ''; + const syncs = [{ + type: syncOptions.iframeEnabled ? 'iframe' : 'image', + url: syncUrl + queryString, + }]; + logInfo('Returning user syncs, type:', syncs[0]?.type); + return syncs; + }; +} diff --git a/modules/adsmartxBidAdapter.js b/modules/adsmartxBidAdapter.js new file mode 100644 index 00000000000..bba9482052a --- /dev/null +++ b/modules/adsmartxBidAdapter.js @@ -0,0 +1,43 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { + createConverter, + isBidRequestValid as validateBidRequest, + createBuildRequests, + interpretResponse as interpretResponseUtil, + createGetUserSyncs, +} from '../libraries/adsmartxUtils/bidderUtils.js'; + +const BIDDER_CODE = 'adsmartx'; +const ENDPOINT_URL = 'https://ads.adsmartx.com/ads/rtb/prebid/js'; +const SYNC_URL = 'https://sync.adsmartx.com/sync'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 60; + +const converter = createConverter({ defaultCurrency: DEFAULT_CURRENCY, defaultTtl: DEFAULT_TTL }); + +const isBidRequestValid = validateBidRequest; +const buildRequests = createBuildRequests( + { converter, endpointUrl: ENDPOINT_URL } +); +const getUserSyncs = createGetUserSyncs(SYNC_URL); + +const interpretResponse = (serverResponse, request) => { + return interpretResponseUtil(serverResponse, request, { + defaultCurrency: DEFAULT_CURRENCY, + defaultTtl: DEFAULT_TTL, + }); +}; + +export const spec = { + code: BIDDER_CODE, + // TODO: set gvlid once confirmed with AI Digital / AdSmartX team + gvlid: undefined, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/adsmartxBidAdapter.md b/modules/adsmartxBidAdapter.md new file mode 100644 index 00000000000..93b3fa6993a --- /dev/null +++ b/modules/adsmartxBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +Module Name : AdSmartX Bidder Adapter +Module Type : Bid Adapter +Maintainer : prebid@aidigital.com + +# Description +Connects to AdSmartX Exchange for bids +AdSmartX supports Display & Video(Instream) currently. + +This adapter is maintained by Smart Exchange, the legal entity behind this implementation. Our official domain is [AI Digital](https://www.aidigital.com/). +# Sample Ad Unit : Banner +``` + var adUnits = [ + { + code: 'test-banner-div', + mediaTypes: { + banner: { + sizes:[ + [320,50] + ] + } + }, + bids:[ + { + bidder: 'adsmartx', + params: { + bidfloor: 0.001, + testMode: 1, + sspId: 123456, + siteId: 987654, + sspUserId: 'u1234' + } + } + ] + } + ] +``` + +# Sample Ad Unit : Video +``` + var videoAdUnit = [ + { + code: 'adsmartx', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', + mimes: ['video/mp4','video/webm'], + minduration: 5, + maxduration: 30, + startdelay: 30, + maxseq: 2, + poddur: 30, + protocols: [1,3,4], + } + }, + bids:[ + { + bidder: 'adsmartx', + params: { + bidfloor: 0.001, + testMode: 1, + sspId: 123456, + siteId: 987654, + sspUserId: 'u1234' + } + } + ] + } + ] +``` diff --git a/modules/risemediatechBidAdapter.js b/modules/risemediatechBidAdapter.js index e3709722ae0..2ac104e3b7c 100644 --- a/modules/risemediatechBidAdapter.js +++ b/modules/risemediatechBidAdapter.js @@ -1,205 +1,38 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { logInfo, logWarn } from '../src/utils.js'; +import { + createConverter, + createBuildRequests, + interpretResponse as interpretResponseUtil, +} from '../libraries/adsmartxUtils/bidderUtils.js'; +import { logWarn } from '../src/utils.js'; const BIDDER_CODE = 'risemediatech'; const ENDPOINT_URL = 'https://dev-ads.risemediatech.com/ads/rtb/prebid/js'; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_TTL = 60; -const converter = ortbConverter({ - context: { - netRevenue: true, - ttl: DEFAULT_TTL, - currency: DEFAULT_CURRENCY, - }, - imp(buildImp, bidRequest, context) { - logInfo('Building impression object for bidRequest:', bidRequest); - const imp = buildImp(bidRequest, context); - const { mediaTypes } = bidRequest; - if (bidRequest.params) { - if (bidRequest.params.bidfloor) { - logInfo('Setting bid floor for impression:', bidRequest.params.bidfloor); - imp.bidfloor = bidRequest.params.bidfloor; - } - } - if (mediaTypes[BANNER]) { - logInfo('Adding banner media type to impression:', mediaTypes[BANNER]); - imp.banner = { format: mediaTypes[BANNER].sizes.map(([w, h]) => ({ w, h })) }; - } else if (mediaTypes[VIDEO]) { - logInfo('Adding video media type to impression:', mediaTypes[VIDEO]); - imp.video = { - ...mediaTypes[VIDEO], - // all video parameters are mapped. - }; - } +const converter = createConverter({ defaultCurrency: DEFAULT_CURRENCY, defaultTtl: DEFAULT_TTL }); - return imp; - }, - request(buildRequest, imps, bidderRequest, context) { - logInfo('Building server request with impressions:', imps); - const request = buildRequest(imps, bidderRequest, context); - request.cur = [DEFAULT_CURRENCY]; - request.tmax = bidderRequest.timeout; - request.test = bidderRequest.test || 0; +export function disableAdapter() { + logWarn('Risemediatech Bid Adapter has been deprecated. Hence disabling this adapter by rejecting bid requests by default.'); + return false; +} - if (Array.isArray(bidderRequest.bids)) { - const hasTestMode = bidderRequest.bids.some(bid => bid.params && bid.params.testMode === 1); - if (hasTestMode) { - request.ext = request.ext || {}; - request.ext.test = 1; - logInfo('Test mode detected in bid params, setting test flag in request:', request.ext.test); - } - } +const isBidRequestValid = disableAdapter(); +const buildRequests = createBuildRequests( + { converter, endpointUrl: ENDPOINT_URL } +); - if (bidderRequest.gdprConsent) { - logInfo('Adding GDPR consent information to request:', bidderRequest.gdprConsent); - request.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; - request.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; - } - - if (bidderRequest.uspConsent) { - logInfo('Adding USP consent information to request:', bidderRequest.uspConsent); - request.regs = request.regs || {}; - request.regs.ext = request.regs.ext || {}; - request.regs.ext.us_privacy = bidderRequest.uspConsent; - } - - return request; - }, -}); - -/** - * Validates the bid request. - * @param {Object} bid - The bid request object. - * @returns {boolean} True if the bid request is valid. - */ -const isBidRequestValid = (bid) => { - logInfo('Validating bid request:', bid); - - const { mediaTypes } = bid; - - // Validate video-specific fields if mediaTypes includes VIDEO - if (mediaTypes?.[VIDEO]) { - const video = mediaTypes[VIDEO]; - - if (!video.mimes || !Array.isArray(video.mimes) || video.mimes.length === 0) { - logWarn('Invalid video bid request: Missing or invalid mimes.'); - return false; - } - if (video.w != null && video.w <= 0) { - logWarn('Invalid video bid request: Invalid width.'); - return false; - } - if (video.h != null && video.h <= 0) { - logWarn('Invalid video bid request: Invalid height.'); - return false; - } - } - - return true; -}; - -/** - * Builds the server request for the bid. - * @param {Array} validBidRequests - Array of valid bid requests. - * @param {Object} bidderRequest - Additional information about the bid request. - * @returns {Object} Server request object. - */ -const buildRequests = (validBidRequests, bidderRequest) => { - logInfo('Building server request for valid bid requests:', validBidRequests); - const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); - logInfo('Converted to ORTB request:', request); - return { - method: 'POST', - url: ENDPOINT_URL, - data: request, - options: { - endpointCompression: true - }, - }; -}; - -/** - * Interprets the server response and extracts bid information. - * @param {Object} serverResponse - The response from the server. - * @param {Object} request - The original request sent to the server. - * @returns {Array} Array of bid objects. - */ const interpretResponse = (serverResponse, request) => { - logInfo('Interpreting server response:', serverResponse); - - const bidResp = serverResponse && serverResponse.body; - if (!bidResp || !Array.isArray(bidResp.seatbid)) { - logWarn('Server response is empty, invalid, or does not contain seatbid array.'); - return []; - } - - const responses = []; - bidResp.seatbid.forEach(seatbid => { - if (Array.isArray(seatbid.bid) && seatbid.bid.length > 0) { - const bid = seatbid.bid[0]; - logInfo('Processing bid response:', bid); - const bidResponse = { - requestId: bid.impid, - cpm: bid.price, - currency: bidResp.cur || DEFAULT_CURRENCY, - width: bid.w, - height: bid.h, - ad: bid.adm, - creativeId: bid.crid, - netRevenue: true, - ttl: DEFAULT_TTL, - meta: { - advertiserDomains: bid.adomain || [], - } - } - - // Set media type based on bid.mtype - if (bid.mtype == null) { - logWarn('Bid response does not contain media type for bidId: ', bid.id); - bidResponse.mediaType = BANNER; - } - switch (bid.mtype) { - case 1: - bidResponse.mediaType = BANNER; - break; - case 2: - bidResponse.mediaType = VIDEO; - bidResponse.vastXml = bid.adm; - break; - default: - logWarn('Unknown media type: ', bid.mtype, ' for bidId: ', bid.id); - break; - } - - // set dealId if present - if (bid.dealid) { - bidResponse.dealId = bid.dealid; - } - logInfo('Interpreted response:', bidResponse, ' for bidId: ', bid.id); - responses.push(bidResponse); - } + return interpretResponseUtil(serverResponse, request, { + defaultCurrency: DEFAULT_CURRENCY, + defaultTtl: DEFAULT_TTL, }); - - logInfo('Interpreted bid responses:', responses); - return responses; }; -/** - * Handles user syncs for GDPR, CCPA, and GPP compliance. - * @param {Object} syncOptions - Options for user sync. - * @param {Array} serverResponses - Server responses. - * @param {Object} gdprConsent - GDPR consent information. - * @param {Object} uspConsent - CCPA consent information. - * @param {Object} gppConsent - GPP consent information. - * @returns {Array} Array of user sync objects. - */ const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - // return [{ type, url }]; - logInfo('User syncs are not implemented in this adapter yet.'); - return null; + return []; }; export const spec = { diff --git a/test/spec/libraries/adsmartxUtils/bidderUtils_spec.js b/test/spec/libraries/adsmartxUtils/bidderUtils_spec.js new file mode 100644 index 00000000000..e2a115bc550 --- /dev/null +++ b/test/spec/libraries/adsmartxUtils/bidderUtils_spec.js @@ -0,0 +1,203 @@ +import { expect } from 'chai'; +import { + getPublisherUserId, + createConverter, + isBidRequestValid, + createBuildRequests, + interpretResponse, + createGetUserSyncs, +} from '../../../../libraries/adsmartxUtils/bidderUtils.js'; +import { BANNER, VIDEO } from '../../../../src/mediaTypes.js'; + +describe('AdSmartX bidderUtils', () => { + const defaultConfig = { defaultCurrency: 'USD', defaultTtl: 60 }; + + describe('getPublisherUserId', () => { + it('returns sspUserId from bid params when present', () => { + const bidParams = { sspUserId: 'user-from-params' }; + const bidderRequest = {}; + expect(getPublisherUserId(bidParams, bidderRequest)).to.equal('user-from-params'); + }); + + it('returns ortb2.user.id when sspUserId not in params', () => { + const bidParams = {}; + const bidderRequest = { ortb2: { user: { id: 'ortb2-user-id' } } }; + expect(getPublisherUserId(bidParams, bidderRequest)).to.equal('ortb2-user-id'); + }); + + it('returns null when neither source has user id', () => { + expect(getPublisherUserId({}, {})).to.equal(null); + expect(getPublisherUserId({}, { ortb2: {} })).to.equal(null); + }); + }); + + describe('createConverter', () => { + it('returns a converter that produces valid ORTB structure', () => { + const converter = createConverter(defaultConfig); + expect(converter).to.be.an('object'); + expect(converter.toORTB).to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + it('returns true for valid banner bid', () => { + const bid = { mediaTypes: { [BANNER]: { sizes: [[300, 250]] } } }; + expect(isBidRequestValid(bid)).to.equal(true); + }); + + it('returns true for valid video bid with mimes and sizes', () => { + const bid = { + mediaTypes: { + [VIDEO]: { mimes: ['video/mp4'], w: 640, h: 480 }, + }, + }; + expect(isBidRequestValid(bid)).to.equal(true); + }); + + it('returns false for video bid with empty mimes', () => { + const bid = { + mediaTypes: { + [VIDEO]: { mimes: [], w: 640, h: 480 }, + }, + }; + expect(isBidRequestValid(bid)).to.equal(false); + }); + + it('returns false for video bid with invalid width', () => { + const bid = { + mediaTypes: { + [VIDEO]: { mimes: ['video/mp4'], w: 0, h: 480 }, + }, + }; + expect(isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('createBuildRequests and interpretResponse', () => { + const endpointUrl = 'https://test.endpoint.com/ads'; + const converter = createConverter(defaultConfig); + const buildRequests = createBuildRequests({ converter, endpointUrl }); + + it('buildRequests returns POST request with endpoint and compressed option', () => { + const validBidRequests = [ + { + bidId: 'bid1', + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: {}, + }, + ]; + const bidderRequest = { timeout: 3000 }; + const result = buildRequests(validBidRequests, bidderRequest); + expect(result.method).to.equal('POST'); + expect(result.url).to.equal(endpointUrl); + expect(result.options).to.deep.include({ endpointCompression: true }); + expect(result.data).to.be.an('object'); + }); + }); + + describe('interpretResponse', () => { + it('returns empty array when body or seatbid missing', () => { + expect(interpretResponse(undefined, {})).to.deep.equal([]); + expect(interpretResponse({ body: {} }, {})).to.deep.equal([]); + expect(interpretResponse({ body: { seatbid: null } }, {})).to.deep.equal([]); + }); + + it('maps seatbid to bids with mediaType from mtype', () => { + const serverResponse = { + body: { + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'imp1', + price: 2.5, + w: 300, + h: 250, + adm: '
Ad
', + crid: 'c1', + adomain: ['example.com'], + mtype: 1, + }, + ], + }, + ], + }, + }; + const bids = interpretResponse(serverResponse, {}, defaultConfig); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('imp1'); + expect(bids[0].mediaType).to.equal(BANNER); + expect(bids[0].currency).to.equal('USD'); + }); + + it('defaults unknown or null mtype to BANNER', () => { + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'imp1', + price: 1, + w: 300, + h: 250, + adm: '
Ad
', + crid: 'c1', + mtype: 999, + }, + ], + }, + ], + }, + }; + const bids = interpretResponse(serverResponse, {}, defaultConfig); + expect(bids[0].mediaType).to.equal(BANNER); + }); + }); + + describe('createGetUserSyncs', () => { + const syncUrl = 'https://ads.example.com/sync'; + + it('returns empty array when iframe and pixel disabled', () => { + const getUserSyncs = createGetUserSyncs(syncUrl); + const result = getUserSyncs( + { iframeEnabled: false, pixelEnabled: false }, + [], + undefined, + undefined, + undefined + ); + expect(result).to.deep.equal([]); + }); + + it('returns sync with URL containing gdpr and iframe_enabled', () => { + const getUserSyncs = createGetUserSyncs(syncUrl); + const result = getUserSyncs( + { iframeEnabled: true, pixelEnabled: false }, + [], + { gdprApplies: true, consentString: 'consent1' }, + undefined, + undefined + ); + expect(result).to.have.lengthOf(1); + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.include(syncUrl); + expect(result[0].url).to.include('gdpr=1'); + expect(result[0].url).to.include('iframe_enabled=true'); + }); + + it('always includes hardcoded ssp_id=630141 and no ssp_site_id', () => { + const getUserSyncs = createGetUserSyncs(syncUrl); + const result = getUserSyncs( + { iframeEnabled: true, pixelEnabled: false }, + [], + undefined, + undefined, + undefined + ); + expect(result[0].url).to.include('ssp_id=630141'); + expect(result[0].url).to.not.include('ssp_site_id'); + }); + }); +}); diff --git a/test/spec/modules/adsmartxBidAdapter_spec.js b/test/spec/modules/adsmartxBidAdapter_spec.js new file mode 100644 index 00000000000..b50599d57c6 --- /dev/null +++ b/test/spec/modules/adsmartxBidAdapter_spec.js @@ -0,0 +1,1089 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adsmartxBidAdapter.js'; + +describe('AdSmartX adapter', () => { + const validBidRequest = { + bidder: 'adsmartx', + params: { + publisherId: '12345', + adSlot: '/1234567/adunit', + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]], + }, + }, + bidId: '1abc', + auctionId: '2def', + }; + + const bidderRequest = { + refererInfo: { + page: 'https://example.com', + }, + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consent123', + }, + uspConsent: '1YNN', + }; + + const serverResponse = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'], + }, + ], + }, + ], + }, + }; + + describe('isBidRequestValid', () => { + it('should return true for valid bid request', () => { + expect(spec.isBidRequestValid(validBidRequest)).to.equal(true); + }); + + it('should return false for invalid video bid request', () => { + const invalidVideoRequest = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: [], + }, + }, + }; + expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + }); + + it('should return false for video bid request with missing mimes', () => { + const invalidVideoRequest = { + ...validBidRequest, + mediaTypes: { + video: { + w: 640, + h: 480 + // mimes missing + } + } + }; + expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + }); + + it('should return false for video request with invalid mimes (not an array)', () => { + const invalidBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: 'video/mp4', // Not an array + w: 640, + h: 480 + } + } + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false for video request with empty mimes array', () => { + const invalidBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: [], + w: 640, + h: 480 + } + } + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false for video request with width <= 0', () => { + const invalidBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 0, + h: 480 + } + } + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false for video request with height <= 0', () => { + const invalidBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 640, + h: -10 + } + } + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false for video bid request with invalid width', () => { + const invalidVideoRequest = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 0, + h: 480 + } + } + }; + expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + }); + + it('should return false for video bid request with invalid height', () => { + const invalidVideoRequest = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 640, + h: 0 + } + } + }; + expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build a valid server request', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + expect(request).to.be.an('object'); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://ads.adsmartx.com/ads/rtb/prebid/js'); + expect(request.data).to.be.an('object'); + }); + + it('should include GDPR and USP consent in the request', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const { regs, user } = request.data; + expect(regs).to.have.property('gdpr', 1); + expect(user).to.have.property('consent', 'consent123'); + expect(regs.ext).to.have.property('us_privacy', '1YNN'); + }); + + it('should include banner impressions in the request', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const { imp } = request.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner'); + expect(imp[0].banner).to.have.property('format').with.lengthOf(2); + }); + + it('should set request.test to 0 if bidderRequest.test is not provided', () => { + const request = spec.buildRequests([validBidRequest], { ...bidderRequest }); + expect(request.data.test).to.equal(0); + }); + + it('should set request.test to bidderRequest.test if provided', () => { + const testBidderRequest = { ...bidderRequest, test: 1 }; + const request = spec.buildRequests([validBidRequest], testBidderRequest); + expect(request.data.test).to.equal(1); + }); + + it('should build a video impression if only video mediaType is present', () => { + const videoBidRequest = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 640, + h: 480 + } + }, + params: { + ...validBidRequest.params, + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + startdelay: 0, + maxseq: 1, + poddur: 60, + protocols: [2, 3] + } + }; + const request = spec.buildRequests([videoBidRequest], bidderRequest); + const { imp } = request.data; + expect(imp[0]).to.have.property('video'); + expect(imp[0]).to.not.have.property('banner'); + expect(imp[0].video).to.include({ w: 640, h: 480 }); + expect(imp[0].video.mimes).to.include('video/mp4'); + }); + + it('should set gdpr to 0 if gdprApplies is false', () => { + const noGdprBidderRequest = { + ...bidderRequest, + gdprConsent: { + gdprApplies: false, + consentString: 'consent123' + } + }; + const request = spec.buildRequests([validBidRequest], noGdprBidderRequest); + expect(request.data.regs).to.have.property('gdpr', 0); + expect(request.data.user).to.have.property('consent', 'consent123'); + }); + + it('should set regs and regs.ext to {} if not already set when only USP consent is present', () => { + const onlyUspBidderRequest = { + ...bidderRequest, + gdprConsent: undefined, + uspConsent: '1YNN' + }; + const request = spec.buildRequests([validBidRequest], onlyUspBidderRequest); + expect(request.data.regs).to.be.an('object'); + expect(request.data.regs.ext).to.be.an('object'); + expect(request.data.regs.ext).to.have.property('us_privacy', '1YNN'); + }); + }); + + describe('interpretResponse', () => { + it('should interpret the server response correctly', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.an('array').with.lengthOf(1); + const bid = bids[0]; + expect(bid).to.have.property('requestId', '1abc'); + expect(bid).to.have.property('cpm', 1.5); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 250); + expect(bid).to.have.property('creativeId', 'creative123'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('netRevenue', true); + expect(bid).to.have.property('ttl', 60); + }); + + it('should return an empty array if no bids are present', () => { + const emptyResponse = { body: { seatbid: [] } }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(emptyResponse, request); + expect(bids).to.be.an('array').with.lengthOf(0); + }); + + it('should interpret multiple seatbids as multiple bids', () => { + const multiSeatbidResponse = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad1
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'], + mtype: 1 + }, + ], + }, + { + bid: [ + { + id: '2bcd', + impid: '2bcd', + price: 2.0, + adm: '
Ad2
', + w: 728, + h: 90, + crid: 'creative456', + adomain: ['another.com'], + mtype: 2 + }, + ], + }, + ], + }, + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(multiSeatbidResponse, request); + expect(bids).to.be.an('array').with.lengthOf(2); + expect(bids[0]).to.have.property('requestId', '1abc'); + expect(bids[1]).to.have.property('requestId', '2bcd'); + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[1].mediaType).to.equal('video'); + expect(bids[0]).to.have.property('cpm', 1.5); + expect(bids[1]).to.have.property('cpm', 2.0); + }); + + it('should set mediaType to banner if mtype is missing', () => { + const responseNoMtype = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'] + // mtype missing + } + ] + } + ] + } + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseNoMtype, request); + expect(bids[0].mediaType).to.equal('banner'); + }); + + it('should set meta.advertiserDomains to an empty array if adomain is missing', () => { + const responseWithoutAdomain = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123' + // adomain is missing + } + ] + } + ] + } + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithoutAdomain, request); + expect(bids[0].meta.advertiserDomains).to.be.an('array').that.is.empty; + }); + + it('should return an empty array and warn if server response is undefined', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(undefined, request); + expect(bids).to.be.an('array').that.is.empty; + }); + + it('should return an empty array and warn if server response body is missing', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse({}, request); + expect(bids).to.be.an('array').that.is.empty; + }); + + it('should return bids from converter if present', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.an('array').with.lengthOf(1); + }); + + it('should log a warning and default mediaType to banner for unknown mtype', () => { + const responseWithUnknownMtype = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'], + mtype: 999, // Unknown mtype + }, + ], + }, + ], + }, + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithUnknownMtype, request); + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0].mediaType).to.equal('banner'); + }); + + it('should include dealId if present in the bid response', () => { + const responseWithDealId = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'], + dealid: 'deal123', + }, + ], + }, + ], + }, + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithDealId, request); + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0]).to.have.property('dealId', 'deal123'); + }); + + it('should handle bids with missing price gracefully', () => { + const responseWithoutPrice = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'], + }, + ], + }, + ], + }, + }; + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithoutPrice, request); + expect(bids).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', () => { + it('should return empty array if neither iframe nor pixel is enabled', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }, [], bidderRequest.gdprConsent, bidderRequest.uspConsent); + expect(syncs).to.be.an('array').that.is.empty; + }); + + it('should return iframe sync when iframeEnabled is true', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: false }, []); + expect(syncs).to.be.an('array').with.lengthOf(1); + expect(syncs[0]).to.have.property('type', 'iframe'); + expect(syncs[0]).to.have.property('url'); + expect(syncs[0].url).to.include('https://sync.adsmartx.com/sync'); + }); + + it('should return image sync when only pixelEnabled is true', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, []); + expect(syncs).to.be.an('array').with.lengthOf(1); + expect(syncs[0]).to.have.property('type', 'image'); + expect(syncs[0]).to.have.property('url'); + }); + + it('should include GDPR consent parameters in sync URL', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + { gdprApplies: true, consentString: 'consent123' } + ); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=consent123'); + }); + + it('should include gdpr=0 when gdprApplies is false', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + { gdprApplies: false, consentString: 'consent123' } + ); + expect(syncs[0].url).to.include('gdpr=0'); + expect(syncs[0].url).to.include('gdpr_consent=consent123'); + }); + + it('should include USP consent parameter in sync URL', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + undefined, + '1YNN' + ); + expect(syncs[0].url).to.include('us_privacy=1YNN'); + }); + + it('should include GPP consent parameters in sync URL', () => { + const gppConsent = { + gppString: 'DBABLA~1YNN', + applicableSections: [7, 8] + }; + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + undefined, + undefined, + gppConsent + ); + expect(syncs[0].url).to.include('gpp=DBABLA~1YNN'); + expect(syncs[0].url).to.include('gpp_sid=7%2C8'); + }); + + it('should not include GPP if gppString is missing', () => { + const gppConsent = { + applicableSections: [7, 8] + }; + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + undefined, + undefined, + gppConsent + ); + expect(syncs[0].url).to.not.include('gpp='); + }); + + it('should not include GPP if applicableSections is empty', () => { + const gppConsent = { + gppString: 'DBABLA~1YNN', + applicableSections: [] + }; + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + undefined, + undefined, + gppConsent + ); + expect(syncs[0].url).to.not.include('gpp='); + }); + + it('should always include hardcoded ssp_id in sync URL', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs[0].url).to.include('ssp_id=630141'); + expect(syncs[0].url).to.not.include('ssp_site_id'); + expect(syncs[0].url).to.not.include('ssp_user_id'); + }); + + it('should include iframe_enabled flag in sync URL', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: false }, []); + expect(syncs[0].url).to.include('iframe_enabled=true'); + }); + + it('should set iframe_enabled=false when only pixel is enabled', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, []); + expect(syncs[0].url).to.include('iframe_enabled=false'); + }); + + it('should include all consent parameters together', () => { + const gppConsent = { + gppString: 'DBABLA~1YNN', + applicableSections: [7] + }; + + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + { gdprApplies: true, consentString: 'consent123' }, + '1YNN', + gppConsent + ); + + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=consent123'); + expect(syncs[0].url).to.include('us_privacy=1YNN'); + expect(syncs[0].url).to.include('gpp='); + expect(syncs[0].url).to.include('gpp_sid=7'); + expect(syncs[0].url).to.include('ssp_id=630141'); + }); + + it('should handle missing GDPR consentString gracefully', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + { gdprApplies: true } + ); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent='); + }); + + it('should retrieve sspUserId from ortb2.user.id when not in bid params', () => { + // sspUserId is no longer forwarded to the sync URL; ssp_id is hardcoded + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs[0].url).to.include('ssp_id=630141'); + expect(syncs[0].url).to.not.include('ssp_user_id'); + }); + + it('should prioritize sspUserId from bid params over ortb2.user.id', () => { + // sspUserId is no longer forwarded to the sync URL; ssp_id is hardcoded + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs[0].url).to.include('ssp_id=630141'); + expect(syncs[0].url).to.not.include('ssp_user_id'); + }); + + it('should always include ssp_id and iframe_enabled in sync URL', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true, pixelEnabled: false }, + [], + undefined, + undefined, + undefined + ); + + expect(syncs).to.be.an('array').with.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include('https://sync.adsmartx.com/sync'); + expect(syncs[0].url).to.include('ssp_id=630141'); + expect(syncs[0].url).to.include('iframe_enabled=true'); + }); + }); + + describe('buildRequests - additional scenarios', () => { + it('should set ext.test to 1 when testMode=1 is in bid params', () => { + const bidWithTestMode = { + ...validBidRequest, + params: { + ...validBidRequest.params, + testMode: 1 + } + }; + + const testBidderRequest = { + ...bidderRequest, + bids: [bidWithTestMode] + }; + + const request = spec.buildRequests([bidWithTestMode], testBidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('test', 1); + }); + + it('should not set ext.test when testMode is not present', () => { + const testBidderRequest = { + ...bidderRequest, + bids: [validBidRequest] + }; + const request = spec.buildRequests([validBidRequest], testBidderRequest); + if (request.data.ext) { + expect(request.data.ext).to.not.have.property('test'); + } + }); + + it('should include sspId in request.ext when present in bid params', () => { + const bidWithSspId = { + ...validBidRequest, + params: { + ...validBidRequest.params, + sspId: 'ssp123' + } + }; + + const testBidderRequest = { + ...bidderRequest, + bids: [bidWithSspId] + }; + + const request = spec.buildRequests([bidWithSspId], testBidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('sspId', 'ssp123'); + }); + + it('should include siteId in request.ext when present in bid params', () => { + const bidWithSiteId = { + ...validBidRequest, + params: { + ...validBidRequest.params, + siteId: 'site456' + } + }; + + const testBidderRequest = { + ...bidderRequest, + bids: [bidWithSiteId] + }; + + const request = spec.buildRequests([bidWithSiteId], testBidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('siteId', 'site456'); + }); + + it('should include both sspId and siteId in request.ext when both present', () => { + const bidWithBothIds = { + ...validBidRequest, + params: { + ...validBidRequest.params, + sspId: 'ssp123', + siteId: 'site456' + } + }; + + const testBidderRequest = { + ...bidderRequest, + bids: [bidWithBothIds] + }; + + const request = spec.buildRequests([bidWithBothIds], testBidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('sspId', 'ssp123'); + expect(request.data.ext).to.have.property('siteId', 'site456'); + }); + + it('should always include hardcoded ssp_id in sync URL regardless of bid params', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs[0].url).to.include('ssp_id=630141'); + expect(syncs[0].url).to.not.include('ssp_site_id'); + expect(syncs[0].url).to.not.include('ssp_user_id'); + }); + + it('should include bidfloor in impression when present in bid params', () => { + const bidWithFloor = { + ...validBidRequest, + params: { + ...validBidRequest.params, + bidfloor: 0.5 + } + }; + + const request = spec.buildRequests([bidWithFloor], bidderRequest); + expect(request.data.imp[0]).to.have.property('bidfloor', 0.5); + }); + + it('should not include bidfloor when not present in bid params', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + expect(request.data.imp[0]).to.not.have.property('bidfloor'); + }); + + it('should set request.tmax to bidderRequest.timeout', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + expect(request.data).to.have.property('tmax', 3000); + }); + + it('should set request.cur to [USD]', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + expect(request.data).to.have.property('cur').that.deep.equals(['USD']); + }); + + it('should enable endpoint compression in options', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + expect(request.options).to.have.property('endpointCompression', true); + }); + + it('should handle empty validBidRequests array gracefully', () => { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('object'); + expect(request.data.imp).to.be.an('array').that.is.empty; + }); + + it('should handle bidderRequest without GDPR consent', () => { + const noBidderRequest = { + ...bidderRequest, + gdprConsent: undefined, + uspConsent: undefined + }; + const request = spec.buildRequests([validBidRequest], noBidderRequest); + expect(request.data).to.not.have.property('regs'); + expect(request.data).to.not.have.property('user'); + }); + }); + + describe('interpretResponse - additional scenarios', () => { + it('should set mediaType to video and include vastXml when mtype is 2', () => { + const videoResponse = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 2.5, + adm: '...', + w: 640, + h: 480, + crid: 'video-creative-123', + adomain: ['video-example.com'], + mtype: 2 + } + ] + } + ] + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(videoResponse, request); + + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0]).to.have.property('mediaType', 'video'); + expect(bids[0]).to.have.property('vastXml', '...'); + }); + + it('should set mediaType to banner when mtype is 1', () => { + const bannerResponse = { + body: { + id: '2def', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Banner Ad
', + w: 300, + h: 250, + crid: 'banner-creative-123', + adomain: ['banner-example.com'], + mtype: 1 + } + ] + } + ] + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(bannerResponse, request); + + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0]).to.have.property('mediaType', 'banner'); + expect(bids[0]).to.not.have.property('vastXml'); + }); + + it('should use custom currency from response if provided', () => { + const responseWithCurrency = { + body: { + id: '2def', + cur: 'EUR', + seatbid: [ + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'] + } + ] + } + ] + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithCurrency, request); + + expect(bids[0]).to.have.property('currency', 'EUR'); + }); + + it('should not include dealId if not present in bid response', () => { + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids[0]).to.not.have.property('dealId'); + }); + + it('should return empty array if seatbid is not an array', () => { + const invalidResponse = { + body: { + id: '2def', + seatbid: 'invalid' + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(invalidResponse, request); + + expect(bids).to.be.an('array').that.is.empty; + }); + + it('should skip seatbid entries with empty bid arrays', () => { + const responseWithEmptyBids = { + body: { + id: '2def', + seatbid: [ + { bid: [] }, + { + bid: [ + { + id: '1abc', + impid: '1abc', + price: 1.5, + adm: '
Ad
', + w: 300, + h: 250, + crid: 'creative123', + adomain: ['example.com'] + } + ] + } + ] + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(responseWithEmptyBids, request); + + expect(bids).to.be.an('array').with.lengthOf(1); + }); + + it('should handle seatbid with non-array bid property', () => { + const invalidSeatbidResponse = { + body: { + id: '2def', + seatbid: [ + { bid: 'invalid' } + ] + } + }; + + const request = spec.buildRequests([validBidRequest], bidderRequest); + const bids = spec.interpretResponse(invalidSeatbidResponse, request); + + expect(bids).to.be.an('array').that.is.empty; + }); + }); + + describe('isBidRequestValid - additional scenarios', () => { + it('should return true for valid video bid with all required fields', () => { + const validVideoBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4', 'video/webm'], + w: 640, + h: 480 + } + } + }; + + expect(spec.isBidRequestValid(validVideoBid)).to.equal(true); + }); + + it('should return true for video bid without width and height (optional)', () => { + const videoBidNoSize = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'] + } + } + }; + + expect(spec.isBidRequestValid(videoBidNoSize)).to.equal(true); + }); + + it('should return true for banner-only bid', () => { + expect(spec.isBidRequestValid(validBidRequest)).to.equal(true); + }); + + it('should return true for bid with both banner and video', () => { + const multiMediaBid = { + ...validBidRequest, + mediaTypes: { + banner: { + sizes: [[300, 250]] + }, + video: { + mimes: ['video/mp4'], + w: 640, + h: 480 + } + } + }; + + expect(spec.isBidRequestValid(multiMediaBid)).to.equal(true); + }); + + it('should return true for video with negative width when width is null', () => { + const videoBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: null, + h: 480 + } + } + }; + + expect(spec.isBidRequestValid(videoBid)).to.equal(true); + }); + + it('should return false for video with width exactly 0', () => { + const videoBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 0, + h: 480 + } + } + }; + + expect(spec.isBidRequestValid(videoBid)).to.equal(false); + }); + + it('should return false for video with negative width', () => { + const videoBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: -100, + h: 480 + } + } + }; + + expect(spec.isBidRequestValid(videoBid)).to.equal(false); + }); + + it('should return false for video with negative height', () => { + const videoBid = { + ...validBidRequest, + mediaTypes: { + video: { + mimes: ['video/mp4'], + w: 640, + h: -100 + } + } + }; + + expect(spec.isBidRequestValid(videoBid)).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/risemediatechBidAdapter_spec.js b/test/spec/modules/risemediatechBidAdapter_spec.js index d4d70017ceb..de9ff2ac32e 100644 --- a/test/spec/modules/risemediatechBidAdapter_spec.js +++ b/test/spec/modules/risemediatechBidAdapter_spec.js @@ -1,5 +1,7 @@ import { expect } from 'chai'; -import { spec } from 'modules/risemediatechBidAdapter.js'; +import sinon from 'sinon'; +import { spec, disableAdapter } from 'modules/risemediatechBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('RiseMediaTech adapter', () => { const validBidRequest = { @@ -51,119 +53,26 @@ describe('RiseMediaTech adapter', () => { }, }; - describe('isBidRequestValid', () => { - it('should return true for valid bid request', () => { - expect(spec.isBidRequestValid(validBidRequest)).to.equal(true); - }); - - it('should return false for invalid video bid request', () => { - const invalidVideoRequest = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: [], - }, - }, - }; - expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); - }); - - it('should return false for video bid request with missing mimes', () => { - const invalidVideoRequest = { - ...validBidRequest, - mediaTypes: { - video: { - w: 640, - h: 480 - // mimes missing - } - } - }; - expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); - }); - - it('should return false for video request with invalid mimes (not an array)', () => { - const invalidBid = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: 'video/mp4', // Not an array - w: 640, - h: 480 - } - } - }; - expect(spec.isBidRequestValid(invalidBid)).to.equal(false); - }); - - it('should return false for video request with empty mimes array', () => { - const invalidBid = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: [], - w: 640, - h: 480 - } - } - }; - expect(spec.isBidRequestValid(invalidBid)).to.equal(false); - }); - - it('should return false for video request with width <= 0', () => { - const invalidBid = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: ['video/mp4'], - w: 0, - h: 480 - } - } - }; - expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + describe('disableAdapter', () => { + it('should log a deprecation warning', () => { + const warnStub = sinon.stub(utils, 'logWarn'); + try { + disableAdapter(); + expect(warnStub.calledOnce).to.be.true; + expect(warnStub.firstCall.args[0]).to.include('deprecated'); + } finally { + warnStub.restore(); + } }); - it('should return false for video request with height <= 0', () => { - const invalidBid = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: ['video/mp4'], - w: 640, - h: -10 - } - } - }; - expect(spec.isBidRequestValid(invalidBid)).to.equal(false); - }); - - it('should return false for video bid request with invalid width', () => { - const invalidVideoRequest = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: ['video/mp4'], - w: 0, - h: 480 - } - } - }; - expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + it('should return false', () => { + expect(disableAdapter()).to.equal(false); }); + }); - it('should return false for video bid request with invalid height', () => { - const invalidVideoRequest = { - ...validBidRequest, - mediaTypes: { - video: { - mimes: ['video/mp4'], - w: 640, - h: 0 - } - } - }; - expect(spec.isBidRequestValid(invalidVideoRequest)).to.equal(false); + describe('isBidRequestValid', () => { + it('should be false because the adapter is disabled/deprecated', () => { + expect(spec.isBidRequestValid).to.equal(false); }); }); @@ -179,8 +88,8 @@ describe('RiseMediaTech adapter', () => { it('should include GDPR and USP consent in the request', () => { const request = spec.buildRequests([validBidRequest], bidderRequest); const { regs, user } = request.data; - expect(regs.ext).to.have.property('gdpr', 1); - expect(user.ext).to.have.property('consent', 'consent123'); + expect(regs).to.have.property('gdpr', 1); + expect(user).to.have.property('consent', 'consent123'); expect(regs.ext).to.have.property('us_privacy', '1YNN'); }); @@ -241,8 +150,8 @@ describe('RiseMediaTech adapter', () => { } }; const request = spec.buildRequests([validBidRequest], noGdprBidderRequest); - expect(request.data.regs.ext).to.have.property('gdpr', 0); - expect(request.data.user.ext).to.have.property('consent', 'consent123'); + expect(request.data.regs).to.have.property('gdpr', 0); + expect(request.data.user).to.have.property('consent', 'consent123'); }); it('should set regs and regs.ext to {} if not already set when only USP consent is present', () => { @@ -403,7 +312,7 @@ describe('RiseMediaTech adapter', () => { expect(bids).to.be.an('array').with.lengthOf(1); }); - it('should log a warning and not set mediaType for unknown mtype', () => { + it('should log a warning and default mediaType to banner for unknown mtype', () => { const responseWithUnknownMtype = { body: { id: '2def', @@ -429,7 +338,7 @@ describe('RiseMediaTech adapter', () => { const request = spec.buildRequests([validBidRequest], bidderRequest); const bids = spec.interpretResponse(responseWithUnknownMtype, request); expect(bids).to.be.an('array').with.lengthOf(1); - expect(bids[0].meta).to.not.have.property('mediaType'); + expect(bids[0].mediaType).to.equal('banner'); }); it('should include dealId if present in the bid response', () => { @@ -484,14 +393,14 @@ describe('RiseMediaTech adapter', () => { }; const request = spec.buildRequests([validBidRequest], bidderRequest); const bids = spec.interpretResponse(responseWithoutPrice, request); - expect(bids).to.be.an('array').that.is.not.empty; + expect(bids).to.be.an('array').that.is.empty; }); }); describe('getUserSyncs', () => { - it('should return null as user syncs are not implemented', () => { + it('should return empty array as user syncs are not implemented', () => { const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], bidderRequest.gdprConsent, bidderRequest.uspConsent); - expect(syncs).to.be.null; + expect(syncs).to.be.an('array').that.is.empty; }); }); });