From 29ae0b6d71ad0fc589e4f73580632b45cf3ebb64 Mon Sep 17 00:00:00 2001 From: Gasper Zagar Date: Tue, 24 Feb 2026 13:39:10 +0100 Subject: [PATCH 01/12] Remove dimension parameter validation for missing values; bump version to v1.1.0 --- modules/ipromBidAdapter.js | 7 ++----- test/spec/modules/ipromBidAdapter_spec.js | 13 ------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 42c7508915c..0825756ef71 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -3,7 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; -const VERSION = 'v1.0.3'; +const VERSION = 'v1.1.0'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; @@ -22,10 +22,7 @@ export const spec = { return false; } // dimension parameter checks - if (!params.dimension) { - logError(`${bidder}: Required parameter 'dimension' missing`); - return false; - } else if (typeof params.dimension !== 'string') { + if (params.dimension && typeof params.dimension !== 'string') { logError(`${bidder}: Parameter 'dimension' needs to be a string`); return false; } diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 3a1a6c972e1..548cb430499 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -68,19 +68,6 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - it('should reject bid if missing dimension', function () { - const invalidBid = { - bidder: 'iprom', - params: { - id: '1234', - } - }; - - const isValid = spec.isBidRequestValid(invalidBid); - - expect(isValid).to.equal(false); - }); - it('should reject bid if dimension is not a string', function () { const invalidBid = { bidder: 'iprom', From add2b6ba02ff3d0d27082bfd927367c3f41471c6 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Wed, 25 Feb 2026 15:35:02 +0100 Subject: [PATCH 02/12] iPROM Adapter: simplify payload fields and add consent/referer warnings --- modules/ipromBidAdapter.js | 81 +++++++++- test/spec/modules/ipromBidAdapter_spec.js | 178 ++++++++++++++++++++-- 2 files changed, 244 insertions(+), 15 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 0825756ef71..2e9726f5bf5 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,4 +1,4 @@ -import { logError } from '../src/utils.js'; +import { deepClone, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'iprom'; @@ -9,6 +9,12 @@ const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; const IAB_GVL_ID = 811; +function logMissingFields(scope, missingFields) { + if (missingFields.length) { + logWarn(`${BIDDER_CODE}: Missing ${scope} fields: ${missingFields.join(', ')}`); + } +} + export const spec = { code: BIDDER_CODE, gvlid: IAB_GVL_ID, @@ -31,12 +37,81 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { + const schain = validBidRequests[0]?.ortb2?.source?.ext?.schain; const payload = { bids: validBidRequests, - // TODO: please do not send internal data structures over the network - referer: bidderRequest.refererInfo.legacy, version: VERSION }; + + if (bidderRequest?.refererInfo) { + const refererInfo = bidderRequest.refererInfo; + const refererUrl = refererInfo.topmostLocation ?? refererInfo.ref; + const missingRefererFields = []; + + if (refererInfo.reachedTop == null) missingRefererFields.push('reachedTop'); + if (refererUrl == null) missingRefererFields.push('referer'); + if (refererInfo.numIframes == null) missingRefererFields.push('numIframes'); + if (refererInfo.stack == null) missingRefererFields.push('stack'); + + logMissingFields('referer', missingRefererFields); + + const referer = { + reachedTop: refererInfo.reachedTop, + referer: refererUrl, + numIframes: refererInfo.numIframes, + stack: refererInfo.stack + }; + + if (Object.values(referer).some(value => value != null)) { + payload.referer = referer; + } + } + + if (bidderRequest?.gdprConsent) { + const tcf = { + consentString: bidderRequest.gdprConsent.consentString, + gdprApplies: bidderRequest.gdprConsent.gdprApplies, + addtlConsent: bidderRequest.gdprConsent.addtlConsent + }; + const missingTcfFields = []; + + if (tcf.consentString == null) missingTcfFields.push('consentString'); + if (tcf.gdprApplies == null) missingTcfFields.push('gdprApplies'); + if (tcf.addtlConsent == null) missingTcfFields.push('addtlConsent'); + + logMissingFields('tcf', missingTcfFields); + + payload.tcf = tcf; + } + + if (schain) { + payload.schain = schain; + } + + if (bidderRequest?.ortb2) { + const firstPartyData = deepClone(bidderRequest.ortb2); + + if (firstPartyData?.source?.ext?.schain) { + delete firstPartyData.source.ext.schain; + + if (!Object.keys(firstPartyData.source.ext).length) { + delete firstPartyData.source.ext; + } + + if (!Object.keys(firstPartyData.source).length) { + delete firstPartyData.source; + } + } + + if (firstPartyData.site) { + delete firstPartyData.site; + } + + if (Object.keys(firstPartyData).length) { + payload.firstPartyData = firstPartyData; + } + } + const payloadString = JSON.stringify(payload); return { diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 548cb430499..8682b4aebea 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -1,4 +1,6 @@ import {expect} from 'chai'; +import sinon from 'sinon'; +import * as utils from 'src/utils.js'; import {spec} from 'modules/ipromBidAdapter.js'; describe('iPROM Adapter', function () { @@ -29,15 +31,13 @@ describe('iPROM Adapter', function () { bidderRequest = { timeout: 3000, refererInfo: { - legacy: { - referer: 'https://adserver.si/index.html', - reachedTop: true, - numIframes: 1, - stack: [ - 'https://adserver.si/index.html', - 'https://adserver.si/iframe1.html', - ] - } + reachedTop: true, + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ], + topmostLocation: 'https://adserver.si/index.html', } } }); @@ -120,12 +120,39 @@ describe('iPROM Adapter', function () { expect(request.url).to.equal('https://core.iprom.net/programmatic'); }); - it('should add referer info', function () { + it('should add only selected referer info', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.referer).to.exist; - expect(requestparse.referer.referer).to.equal('https://adserver.si/index.html'); + expect(requestparse.referer).to.deep.equal({ + reachedTop: true, + referer: 'https://adserver.si/index.html', + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ] + }); + expect(requestparse.referer.canonicalUrl).to.be.undefined; + expect(requestparse.referer.legacy).to.be.undefined; + }); + + it('should warn if referer fields are missing', function () { + const warnSpy = sinon.spy(utils, 'logWarn'); + + const bidderRequestWithMissingRefererFields = { + ...bidderRequest, + refererInfo: { + reachedTop: true + } + }; + + spec.buildRequests(bidRequests, bidderRequestWithMissingRefererFields); + + expect(warnSpy.calledOnce).to.equal(true); + expect(warnSpy.firstCall.args[0]).to.equal('iprom: Missing referer fields: referer, numIframes, stack'); + + warnSpy.restore(); }); it('should add adapter version', function () { @@ -135,6 +162,133 @@ describe('iPROM Adapter', function () { expect(requestparse.version).to.exist; }); + it('should add TCF data', function () { + const bidderRequestWithTcf = { + ...bidderRequest, + gdprConsent: { + consentString: 'consent-string', + gdprApplies: true, + addtlConsent: 'addtl-consent' + } + }; + const request = spec.buildRequests(bidRequests, bidderRequestWithTcf); + const requestparse = JSON.parse(request.data); + + expect(requestparse.tcf).to.deep.equal({ + consentString: 'consent-string', + gdprApplies: true, + addtlConsent: 'addtl-consent' + }); + }); + + it('should warn if TCF fields are missing', function () { + const warnSpy = sinon.spy(utils, 'logWarn'); + const bidderRequestWithIncompleteTcf = { + ...bidderRequest, + gdprConsent: { + consentString: 'consent-string' + } + }; + + spec.buildRequests(bidRequests, bidderRequestWithIncompleteTcf); + + expect(warnSpy.calledOnce).to.equal(true); + expect(warnSpy.firstCall.args[0]).to.equal('iprom: Missing tcf fields: gdprApplies, addtlConsent'); + + warnSpy.restore(); + }); + + it('should add schain data', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + const bidRequestsWithSchain = [{ + ...bidRequests[0], + ortb2: { + source: { + ext: { + schain + } + } + } + }]; + const request = spec.buildRequests(bidRequestsWithSchain, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.schain).to.deep.equal(schain); + }); + + it('should keep schain only at top level', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + const bidRequestsWithSchain = [{ + ...bidRequests[0], + ortb2: { + source: { + ext: { + schain + } + } + } + }]; + const bidderRequestWithFpd = { + ...bidderRequest, + ortb2: { + site: { + domain: 'adserver.si' + } + } + }; + const request = spec.buildRequests(bidRequestsWithSchain, bidderRequestWithFpd); + const requestparse = JSON.parse(request.data); + + expect(requestparse.schain).to.deep.equal(schain); + expect(requestparse.firstPartyData).to.be.undefined; + }); + + it('should add first party data', function () { + const firstPartyData = { + site: { + domain: 'adserver.si', + page: 'https://adserver.si/index.html' + }, + user: { + data: [{ + name: 'taxonomy', + segment: [{id: 'segment-id'}] + }] + } + }; + const bidderRequestWithFpd = { + ...bidderRequest, + ortb2: firstPartyData + }; + const request = spec.buildRequests(bidRequests, bidderRequestWithFpd); + const requestparse = JSON.parse(request.data); + + expect(requestparse.firstPartyData).to.deep.equal({ + user: { + data: [{ + name: 'taxonomy', + segment: [{id: 'segment-id'}] + }] + } + }); + }); + it('should contain id and dimension', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); From d12ad8e801dd0d82c6dd212f15000881eacbff18 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Wed, 25 Feb 2026 15:51:23 +0100 Subject: [PATCH 03/12] iPROM Adapter: support custom endpoint with ORTB request/response --- modules/ipromBidAdapter.js | 58 ++++++++++ test/spec/modules/ipromBidAdapter_spec.js | 122 ++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 2e9726f5bf5..54d4560a11e 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,5 +1,6 @@ import { deepClone, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; @@ -8,6 +9,12 @@ const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; const IAB_GVL_ID = 811; +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NETREVENUE, + ttl: DEFAULT_TTL + } +}); function logMissingFields(scope, missingFields) { if (missingFields.length) { @@ -15,6 +22,25 @@ function logMissingFields(scope, missingFields) { } } +function isValidEndpointUrl(endpoint) { + try { + const parsedEndpoint = new URL(endpoint); + return parsedEndpoint.protocol === 'http:' || parsedEndpoint.protocol === 'https:'; + } catch (e) { + return false; + } +} + +function getCustomEndpoint(validBidRequests) { + const endpoint = validBidRequests?.[0]?.params?.endpoint; + + if (typeof endpoint === 'string' && isValidEndpointUrl(endpoint)) { + return endpoint; + } + + return null; +} + export const spec = { code: BIDDER_CODE, gvlid: IAB_GVL_ID, @@ -33,10 +59,38 @@ export const spec = { return false; } + if (params.endpoint != null) { + if (typeof params.endpoint !== 'string') { + logError(`${bidder}: Parameter 'endpoint' needs to be a string`); + return false; + } + + if (!isValidEndpointUrl(params.endpoint)) { + logError(`${bidder}: Parameter 'endpoint' needs to be a valid URL`); + return false; + } + } + return true; }, buildRequests: function (validBidRequests, bidderRequest) { + const customEndpoint = getCustomEndpoint(validBidRequests); + + if (customEndpoint) { + const ortbRequest = converter.toORTB({ + bidderRequest, + bidRequests: validBidRequests, + }); + + return { + method: 'POST', + url: customEndpoint, + data: ortbRequest, + ortb: true + }; + } + const schain = validBidRequests[0]?.ortb2?.source?.ext?.schain; const payload = { bids: validBidRequests, @@ -122,6 +176,10 @@ export const spec = { }, interpretResponse: function (serverResponse, request) { + if (request?.ortb) { + return converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + } + const bids = serverResponse.body; const bidResponses = []; diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 8682b4aebea..5eb278be68f 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -108,6 +108,48 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); + + it('should accept bid with valid endpoint url', function () { + const validBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 'https://custom.iprom.net/programmatic' + } + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject bid if endpoint is not a string', function () { + const invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 1234 + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if endpoint is not a valid url', function () { + const invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 'invalid-url' + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); }); describe('building requests', function () { @@ -120,6 +162,49 @@ describe('iPROM Adapter', function () { expect(request.url).to.equal('https://core.iprom.net/programmatic'); }); + it('should use custom endpoint from params when valid', function () { + const bidRequestsWithEndpoint = [{ + ...bidRequests[0], + params: { + ...bidRequests[0].params, + endpoint: 'https://custom.iprom.net/programmatic' + } + }]; + const request = spec.buildRequests(bidRequestsWithEndpoint, bidderRequest); + + expect(request.url).to.equal('https://custom.iprom.net/programmatic'); + expect(request.ortb).to.equal(true); + expect(request.data).to.be.an('object'); + expect(request.data.imp).to.be.an('array').with.lengthOf(1); + expect(request.data.bids).to.be.undefined; + }); + + it('should fallback to default endpoint for invalid custom endpoint', function () { + const bidRequestsWithInvalidEndpoint = [{ + ...bidRequests[0], + params: { + ...bidRequests[0].params, + endpoint: 'invalid-url' + } + }]; + const request = spec.buildRequests(bidRequestsWithInvalidEndpoint, bidderRequest); + + expect(request.url).to.equal('https://core.iprom.net/programmatic'); + }); + + it('should fallback to default endpoint for non-string endpoint', function () { + const bidRequestsWithNonStringEndpoint = [{ + ...bidRequests[0], + params: { + ...bidRequests[0].params, + endpoint: 1234 + } + }]; + const request = spec.buildRequests(bidRequestsWithNonStringEndpoint, bidderRequest); + + expect(request.url).to.equal('https://core.iprom.net/programmatic'); + }); + it('should add only selected referer info', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); @@ -324,6 +409,43 @@ describe('iPROM Adapter', function () { expect(bids[0].meta.advertiserDomains).to.deep.equal(['https://example.com']); }); + it('should parse OpenRTB response when custom endpoint is used', function () { + const bidRequestsWithEndpoint = [{ + ...bidRequests[0], + params: { + ...bidRequests[0].params, + endpoint: 'https://custom.iprom.net/programmatic' + } + }]; + const request = spec.buildRequests(bidRequestsWithEndpoint, bidderRequest); + const impId = request.data.imp[0].id; + const serverResponse = { + body: { + cur: 'EUR', + seatbid: [{ + bid: [{ + impid: impId, + price: 0.8, + w: 300, + h: 250, + crid: 'creative-1', + adomain: ['example.com'], + adm: '
ad
', + mtype: 1 + }] + }] + } + }; + + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].requestId).to.equal('29a72b151f7bd3'); + expect(bids[0].cpm).to.equal(0.8); + expect(bids[0].currency).to.equal('EUR'); + expect(bids[0].meta.advertiserDomains).to.deep.equal(['example.com']); + }); + it('should return empty bid response', function () { const emptyServerResponse = { body: [] From 6a310faf52d9e051f2429b16243354bbc6d7b5f6 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Thu, 26 Feb 2026 12:21:59 +0100 Subject: [PATCH 04/12] iPROM Adapter: move custom endpoint to bidder config only --- modules/ipromBidAdapter.js | 25 +--- test/spec/modules/ipromBidAdapter_spec.js | 149 ++++++++-------------- 2 files changed, 61 insertions(+), 113 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 54d4560a11e..2a871de02e2 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,5 +1,6 @@ import { deepClone, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'iprom'; @@ -31,11 +32,11 @@ function isValidEndpointUrl(endpoint) { } } -function getCustomEndpoint(validBidRequests) { - const endpoint = validBidRequests?.[0]?.params?.endpoint; +function getCustomEndpoint() { + const configuredEndpoint = config.getConfig(`${BIDDER_CODE}.endpoint`); - if (typeof endpoint === 'string' && isValidEndpointUrl(endpoint)) { - return endpoint; + if (typeof configuredEndpoint === 'string' && isValidEndpointUrl(configuredEndpoint)) { + return configuredEndpoint; } return null; @@ -59,23 +60,11 @@ export const spec = { return false; } - if (params.endpoint != null) { - if (typeof params.endpoint !== 'string') { - logError(`${bidder}: Parameter 'endpoint' needs to be a string`); - return false; - } - - if (!isValidEndpointUrl(params.endpoint)) { - logError(`${bidder}: Parameter 'endpoint' needs to be a valid URL`); - return false; - } - } - return true; }, buildRequests: function (validBidRequests, bidderRequest) { - const customEndpoint = getCustomEndpoint(validBidRequests); + const customEndpoint = getCustomEndpoint(); if (customEndpoint) { const ortbRequest = converter.toORTB({ @@ -91,7 +80,6 @@ export const spec = { }; } - const schain = validBidRequests[0]?.ortb2?.source?.ext?.schain; const payload = { bids: validBidRequests, version: VERSION @@ -138,6 +126,7 @@ export const spec = { payload.tcf = tcf; } + const schain = bidderRequest?.ortb2?.source?.ext?.schain; if (schain) { payload.schain = schain; } diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 5eb278be68f..f0685115c1d 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import sinon from 'sinon'; import * as utils from 'src/utils.js'; +import {config} from 'src/config.js'; import {spec} from 'modules/ipromBidAdapter.js'; describe('iPROM Adapter', function () { @@ -8,6 +9,7 @@ describe('iPROM Adapter', function () { let bidderRequest; beforeEach(function () { + config.resetConfig(); bidRequests = [ { bidder: 'iprom', @@ -42,13 +44,16 @@ describe('iPROM Adapter', function () { } }); + afterEach(function () { + config.resetConfig(); + }); + describe('validating bids', function () { - it('should accept valid bid', function () { + it('should accept valid bid with only id', function () { const validBid = { bidder: 'iprom', params: { - id: '1234', - dimension: '300x250', + id: '1234' }, }; @@ -108,48 +113,6 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); - - it('should accept bid with valid endpoint url', function () { - const validBid = { - bidder: 'iprom', - params: { - id: '1234', - endpoint: 'https://custom.iprom.net/programmatic' - } - }; - - const isValid = spec.isBidRequestValid(validBid); - - expect(isValid).to.equal(true); - }); - - it('should reject bid if endpoint is not a string', function () { - const invalidBid = { - bidder: 'iprom', - params: { - id: '1234', - endpoint: 1234 - } - }; - - const isValid = spec.isBidRequestValid(invalidBid); - - expect(isValid).to.equal(false); - }); - - it('should reject bid if endpoint is not a valid url', function () { - const invalidBid = { - bidder: 'iprom', - params: { - id: '1234', - endpoint: 'invalid-url' - } - }; - - const isValid = spec.isBidRequestValid(invalidBid); - - expect(isValid).to.equal(false); - }); }); describe('building requests', function () { @@ -162,47 +125,50 @@ describe('iPROM Adapter', function () { expect(request.url).to.equal('https://core.iprom.net/programmatic'); }); - it('should use custom endpoint from params when valid', function () { - const bidRequestsWithEndpoint = [{ - ...bidRequests[0], - params: { - ...bidRequests[0].params, - endpoint: 'https://custom.iprom.net/programmatic' + it('should use custom endpoint from global bidder config', function () { + config.setConfig({ + iprom: { + endpoint: 'https://global.iprom.net/programmatic' } - }]; - const request = spec.buildRequests(bidRequestsWithEndpoint, bidderRequest); + }); + const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.equal('https://custom.iprom.net/programmatic'); + expect(request.url).to.equal('https://global.iprom.net/programmatic'); expect(request.ortb).to.equal(true); expect(request.data).to.be.an('object'); expect(request.data.imp).to.be.an('array').with.lengthOf(1); expect(request.data.bids).to.be.undefined; }); - it('should fallback to default endpoint for invalid custom endpoint', function () { - const bidRequestsWithInvalidEndpoint = [{ - ...bidRequests[0], - params: { - ...bidRequests[0].params, - endpoint: 'invalid-url' + it('should include schain in ORTB request when present in bidderRequest.ortb2', function () { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '00001', + hp: 1 + }] + }; + config.setConfig({ + iprom: { + endpoint: 'https://global.iprom.net/programmatic' } - }]; - const request = spec.buildRequests(bidRequestsWithInvalidEndpoint, bidderRequest); - - expect(request.url).to.equal('https://core.iprom.net/programmatic'); - }); - - it('should fallback to default endpoint for non-string endpoint', function () { - const bidRequestsWithNonStringEndpoint = [{ - ...bidRequests[0], - params: { - ...bidRequests[0].params, - endpoint: 1234 + }); + const bidderRequestWithGlobalSchain = { + ...bidderRequest, + ortb2: { + source: { + ext: { + schain + } + } } - }]; - const request = spec.buildRequests(bidRequestsWithNonStringEndpoint, bidderRequest); + }; - expect(request.url).to.equal('https://core.iprom.net/programmatic'); + const request = spec.buildRequests(bidRequests, bidderRequestWithGlobalSchain); + + expect(request.data.source.ext.schain).to.deep.equal(schain); }); it('should add only selected referer info', function () { @@ -293,8 +259,8 @@ describe('iPROM Adapter', function () { hp: 1 }] }; - const bidRequestsWithSchain = [{ - ...bidRequests[0], + const bidderRequestWithSchain = { + ...bidderRequest, ortb2: { source: { ext: { @@ -302,8 +268,8 @@ describe('iPROM Adapter', function () { } } } - }]; - const request = spec.buildRequests(bidRequestsWithSchain, bidderRequest); + }; + const request = spec.buildRequests(bidRequests, bidderRequestWithSchain); const requestparse = JSON.parse(request.data); expect(requestparse.schain).to.deep.equal(schain); @@ -319,25 +285,20 @@ describe('iPROM Adapter', function () { hp: 1 }] }; - const bidRequestsWithSchain = [{ - ...bidRequests[0], + const bidderRequestWithFpdAndSchain = { + ...bidderRequest, ortb2: { source: { ext: { schain } - } - } - }]; - const bidderRequestWithFpd = { - ...bidderRequest, - ortb2: { + }, site: { domain: 'adserver.si' } } }; - const request = spec.buildRequests(bidRequestsWithSchain, bidderRequestWithFpd); + const request = spec.buildRequests(bidRequests, bidderRequestWithFpdAndSchain); const requestparse = JSON.parse(request.data); expect(requestparse.schain).to.deep.equal(schain); @@ -410,14 +371,12 @@ describe('iPROM Adapter', function () { }); it('should parse OpenRTB response when custom endpoint is used', function () { - const bidRequestsWithEndpoint = [{ - ...bidRequests[0], - params: { - ...bidRequests[0].params, - endpoint: 'https://custom.iprom.net/programmatic' + config.setConfig({ + iprom: { + endpoint: 'https://global.iprom.net/programmatic' } - }]; - const request = spec.buildRequests(bidRequestsWithEndpoint, bidderRequest); + }); + const request = spec.buildRequests(bidRequests, bidderRequest); const impId = request.data.imp[0].id; const serverResponse = { body: { From ab34395f4804f744be9b0f328638b1d697ba7a40 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Thu, 26 Feb 2026 13:02:24 +0100 Subject: [PATCH 05/12] iPROM Adapter: refactor request building and response handling --- modules/ipromBidAdapter.js | 251 +++++++++++++--------- test/spec/modules/ipromBidAdapter_spec.js | 48 +++++ 2 files changed, 192 insertions(+), 107 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 2a871de02e2..5e8dea3dbda 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -42,160 +42,197 @@ function getCustomEndpoint() { return null; } -export const spec = { - code: BIDDER_CODE, - gvlid: IAB_GVL_ID, - isBidRequestValid: function ({ bidder, params = {} } = {}) { - // id parameter checks - if (!params.id) { - logError(`${bidder}: Parameter 'id' missing`); - return false; - } else if (typeof params.id !== 'string') { - logError(`${bidder}: Parameter 'id' needs to be a string`); - return false; - } - // dimension parameter checks - if (params.dimension && typeof params.dimension !== 'string') { - logError(`${bidder}: Parameter 'dimension' needs to be a string`); - return false; - } +function extractReferer(refererInfo) { + if (!refererInfo) { + return null; + } - return true; - }, + const refererUrl = refererInfo.topmostLocation ?? refererInfo.ref; + const missingRefererFields = []; - buildRequests: function (validBidRequests, bidderRequest) { - const customEndpoint = getCustomEndpoint(); + if (refererInfo.reachedTop == null) missingRefererFields.push('reachedTop'); + if (refererUrl == null) missingRefererFields.push('referer'); + if (refererInfo.numIframes == null) missingRefererFields.push('numIframes'); + if (refererInfo.stack == null) missingRefererFields.push('stack'); - if (customEndpoint) { - const ortbRequest = converter.toORTB({ - bidderRequest, - bidRequests: validBidRequests, - }); - - return { - method: 'POST', - url: customEndpoint, - data: ortbRequest, - ortb: true - }; - } + logMissingFields('referer', missingRefererFields); - const payload = { - bids: validBidRequests, - version: VERSION - }; + const referer = { + reachedTop: refererInfo.reachedTop, + referer: refererUrl, + numIframes: refererInfo.numIframes, + stack: refererInfo.stack + }; - if (bidderRequest?.refererInfo) { - const refererInfo = bidderRequest.refererInfo; - const refererUrl = refererInfo.topmostLocation ?? refererInfo.ref; - const missingRefererFields = []; + return Object.values(referer).some(value => value != null) ? referer : null; +} - if (refererInfo.reachedTop == null) missingRefererFields.push('reachedTop'); - if (refererUrl == null) missingRefererFields.push('referer'); - if (refererInfo.numIframes == null) missingRefererFields.push('numIframes'); - if (refererInfo.stack == null) missingRefererFields.push('stack'); +function extractTcf(gdprConsent) { + if (!gdprConsent) { + return null; + } - logMissingFields('referer', missingRefererFields); + const tcf = { + consentString: gdprConsent.consentString, + gdprApplies: gdprConsent.gdprApplies, + addtlConsent: gdprConsent.addtlConsent + }; + const missingTcfFields = []; - const referer = { - reachedTop: refererInfo.reachedTop, - referer: refererUrl, - numIframes: refererInfo.numIframes, - stack: refererInfo.stack - }; + if (tcf.consentString == null) missingTcfFields.push('consentString'); + if (tcf.gdprApplies == null) missingTcfFields.push('gdprApplies'); + if (tcf.addtlConsent == null) missingTcfFields.push('addtlConsent'); - if (Object.values(referer).some(value => value != null)) { - payload.referer = referer; - } - } + logMissingFields('tcf', missingTcfFields); - if (bidderRequest?.gdprConsent) { - const tcf = { - consentString: bidderRequest.gdprConsent.consentString, - gdprApplies: bidderRequest.gdprConsent.gdprApplies, - addtlConsent: bidderRequest.gdprConsent.addtlConsent - }; - const missingTcfFields = []; + return tcf; +} - if (tcf.consentString == null) missingTcfFields.push('consentString'); - if (tcf.gdprApplies == null) missingTcfFields.push('gdprApplies'); - if (tcf.addtlConsent == null) missingTcfFields.push('addtlConsent'); +function removeSchainFromFirstPartyData(firstPartyData) { + if (!firstPartyData?.source?.ext?.schain) { + return; + } - logMissingFields('tcf', missingTcfFields); + delete firstPartyData.source.ext.schain; - payload.tcf = tcf; - } + if (!Object.keys(firstPartyData.source.ext).length) { + delete firstPartyData.source.ext; + } - const schain = bidderRequest?.ortb2?.source?.ext?.schain; - if (schain) { - payload.schain = schain; - } + if (!Object.keys(firstPartyData.source).length) { + delete firstPartyData.source; + } +} - if (bidderRequest?.ortb2) { - const firstPartyData = deepClone(bidderRequest.ortb2); +function extractFirstPartyData(ortb2) { + if (!ortb2) { + return null; + } - if (firstPartyData?.source?.ext?.schain) { - delete firstPartyData.source.ext.schain; + const firstPartyData = deepClone(ortb2); - if (!Object.keys(firstPartyData.source.ext).length) { - delete firstPartyData.source.ext; - } + removeSchainFromFirstPartyData(firstPartyData); - if (!Object.keys(firstPartyData.source).length) { - delete firstPartyData.source; - } - } + if (firstPartyData.site) { + delete firstPartyData.site; + } - if (firstPartyData.site) { - delete firstPartyData.site; - } + return Object.keys(firstPartyData).length ? firstPartyData : null; +} - if (Object.keys(firstPartyData).length) { - payload.firstPartyData = firstPartyData; - } +function buildLegacyPayload(validBidRequests, bidderRequest) { + const payload = { + bids: validBidRequests, + version: VERSION + }; + + const referer = extractReferer(bidderRequest?.refererInfo); + if (referer) { + payload.referer = referer; + } + + const tcf = extractTcf(bidderRequest?.gdprConsent); + if (tcf) { + payload.tcf = tcf; + } + + const schain = bidderRequest?.ortb2?.source?.ext?.schain; + if (schain) { + payload.schain = schain; + } + + const firstPartyData = extractFirstPartyData(bidderRequest?.ortb2); + if (firstPartyData) { + payload.firstPartyData = firstPartyData; + } + + return payload; +} + +function buildOrtbRequest(validBidRequests, bidderRequest, endpoint) { + const ortbRequest = converter.toORTB({ + bidderRequest, + bidRequests: validBidRequests, + }); + + return { + method: 'POST', + url: endpoint, + data: ortbRequest, + ortb: true + }; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: IAB_GVL_ID, + isBidRequestValid: function ({ bidder, params = {} } = {}) { + const bidderName = bidder || BIDDER_CODE; + + if (!params.id) { + logError(`${bidderName}: Parameter 'id' missing`); + return false; } - const payloadString = JSON.stringify(payload); + if (typeof params.id !== 'string') { + logError(`${bidderName}: Parameter 'id' needs to be a string`); + return false; + } + + if (params.dimension && typeof params.dimension !== 'string') { + logError(`${bidderName}: Parameter 'dimension' needs to be a string`); + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const customEndpoint = getCustomEndpoint(); + + if (customEndpoint) { + // ORTB mode uses an object payload and is interpreted via converter.fromORTB. + return buildOrtbRequest(validBidRequests, bidderRequest, customEndpoint); + } + + // Legacy mode uses a stringified JSON payload. + const payload = buildLegacyPayload(validBidRequests, bidderRequest); return { method: 'POST', url: ENDPOINT_URL, - data: payloadString + data: JSON.stringify(payload) }; }, interpretResponse: function (serverResponse, request) { if (request?.ortb) { - return converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + return converter.fromORTB({response: serverResponse?.body, request: request.data}).bids ?? []; } - const bids = serverResponse.body; - - const bidResponses = []; + const bids = Array.isArray(serverResponse?.body) ? serverResponse.body : []; - bids.forEach(bid => { - const b = { + return bids.map((bid) => { + const responseBid = { ad: bid.ad, requestId: bid.requestId, cpm: bid.cpm, width: bid.width, height: bid.height, creativeId: bid.creativeId, - currency: bid.currency || DEFAULT_CURRENCY, - netRevenue: bid.netRevenue || DEFAULT_NETREVENUE, - ttl: bid.ttl || DEFAULT_TTL, + currency: bid.currency ?? DEFAULT_CURRENCY, + netRevenue: bid.netRevenue ?? DEFAULT_NETREVENUE, + ttl: bid.ttl ?? DEFAULT_TTL, meta: {}, }; if (bid.aDomains && bid.aDomains.length) { - b.meta.advertiserDomains = bid.aDomains; + responseBid.meta.advertiserDomains = bid.aDomains; } - bidResponses.push(b); + return responseBid; }); - - return bidResponses; }, -} +}; registerBidder(spec); diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index f0685115c1d..3cf393c9727 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -140,6 +140,19 @@ describe('iPROM Adapter', function () { expect(request.data.bids).to.be.undefined; }); + it('should ignore invalid custom endpoint and use default endpoint', function () { + config.setConfig({ + iprom: { + endpoint: 'not-a-valid-url' + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.url).to.equal('https://core.iprom.net/programmatic'); + expect(request.ortb).to.be.undefined; + expect(request.data).to.be.a('string'); + }); + it('should include schain in ORTB request when present in bidderRequest.ortb2', function () { const schain = { ver: '1.0', @@ -370,6 +383,30 @@ describe('iPROM Adapter', function () { expect(bids[0].meta.advertiserDomains).to.deep.equal(['https://example.com']); }); + it('should preserve false netRevenue and zero ttl from response', function () { + const serverResponse = { + body: [{ + requestId: '29a72b151f7bd3', + cpm: 0.5, + width: 300, + height: 250, + creativeId: 1234, + ad: '
ad
', + netRevenue: false, + ttl: 0, + currency: 'USD' + }] + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].netRevenue).to.equal(false); + expect(bids[0].ttl).to.equal(0); + expect(bids[0].currency).to.equal('USD'); + }); + it('should parse OpenRTB response when custom endpoint is used', function () { config.setConfig({ iprom: { @@ -405,6 +442,17 @@ describe('iPROM Adapter', function () { expect(bids[0].meta.advertiserDomains).to.deep.equal(['example.com']); }); + it('should return empty bid response when body is not an array', function () { + const malformedServerResponse = { + body: {} + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(malformedServerResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + it('should return empty bid response', function () { const emptyServerResponse = { body: [] From 0f3bb151fabe7b2bb3df3c8d8f09cd5985dd4e2b Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Fri, 6 Mar 2026 13:02:19 +0100 Subject: [PATCH 06/12] iPROM Adapter: move endpoint and ortb settings to ad unit params with request grouping Move endpoint and ortb config from bidder-level config to per-ad-unit params. buildRequests now groups bids by (endpoint, ortb) and returns an array of requests, allowing different ad units to target different endpoints with different payload formats independently. Co-Authored-By: Claude Opus 4.6 --- modules/ipromBidAdapter.js | 57 +++-- test/spec/modules/ipromBidAdapter_spec.js | 259 ++++++++++++++++------ 2 files changed, 233 insertions(+), 83 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 5e8dea3dbda..38e09cda5be 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,6 +1,5 @@ import { deepClone, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'iprom'; @@ -32,14 +31,12 @@ function isValidEndpointUrl(endpoint) { } } -function getCustomEndpoint() { - const configuredEndpoint = config.getConfig(`${BIDDER_CODE}.endpoint`); - - if (typeof configuredEndpoint === 'string' && isValidEndpointUrl(configuredEndpoint)) { - return configuredEndpoint; +function resolveEndpoint(endpoint) { + if (typeof endpoint === 'string' && isValidEndpointUrl(endpoint)) { + return endpoint; } - return null; + return ENDPOINT_URL; } function extractReferer(refererInfo) { @@ -184,25 +181,49 @@ export const spec = { return false; } + if (params.endpoint !== undefined && !isValidEndpointUrl(params.endpoint)) { + logError(`${bidderName}: Parameter 'endpoint' needs to be a valid URL`); + return false; + } + + if (params.ortb !== undefined && typeof params.ortb !== 'boolean') { + logError(`${bidderName}: Parameter 'ortb' needs to be a boolean`); + return false; + } + return true; }, buildRequests: function (validBidRequests, bidderRequest) { - const customEndpoint = getCustomEndpoint(); + const groups = {}; - if (customEndpoint) { - // ORTB mode uses an object payload and is interpreted via converter.fromORTB. - return buildOrtbRequest(validBidRequests, bidderRequest, customEndpoint); + for (const bid of validBidRequests) { + const endpoint = resolveEndpoint(bid.params.endpoint); + const ortb = bid.params.ortb === true; + const key = `${endpoint}::${ortb}`; + + if (!groups[key]) { + groups[key] = { endpoint, ortb, bids: [] }; + } + + groups[key].bids.push(bid); } - // Legacy mode uses a stringified JSON payload. - const payload = buildLegacyPayload(validBidRequests, bidderRequest); + return Object.values(groups).map(({ endpoint, ortb, bids }) => { + if (ortb) { + // ORTB mode uses an object payload and is interpreted via converter.fromORTB. + return buildOrtbRequest(bids, bidderRequest, endpoint); + } - return { - method: 'POST', - url: ENDPOINT_URL, - data: JSON.stringify(payload) - }; + // Legacy mode uses a stringified JSON payload. + const payload = buildLegacyPayload(bids, bidderRequest); + + return { + method: 'POST', + url: endpoint, + data: JSON.stringify(payload) + }; + }); }, interpretResponse: function (serverResponse, request) { diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 3cf393c9727..a314e98eca5 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -113,44 +113,113 @@ describe('iPROM Adapter', function () { expect(isValid).to.equal(false); }); + + it('should reject bid if endpoint param is not a valid URL', function () { + const invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 'not-a-valid-url', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should accept bid if endpoint param is a valid URL', function () { + const validBid = { + bidder: 'iprom', + params: { + id: '1234', + endpoint: 'https://custom.iprom.net/programmatic', + } + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject bid if ortb param is not a boolean', function () { + const invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + ortb: 'true', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should accept bid if ortb param is true', function () { + const validBid = { + bidder: 'iprom', + params: { + id: '1234', + ortb: true, + } + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); }); describe('building requests', function () { it('should go to correct endpoint', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequest); - expect(request.method).to.exist; - expect(request.method).to.equal('POST'); - expect(request.url).to.exist; - expect(request.url).to.equal('https://core.iprom.net/programmatic'); + expect(requests).to.be.an('array').with.lengthOf(1); + expect(requests[0].method).to.exist; + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.exist; + expect(requests[0].url).to.equal('https://core.iprom.net/programmatic'); }); - it('should use custom endpoint from global bidder config', function () { - config.setConfig({ - iprom: { - endpoint: 'https://global.iprom.net/programmatic' - } - }); - const request = spec.buildRequests(bidRequests, bidderRequest); + it('should use custom endpoint from ad unit params with legacy format', function () { + const bidRequestsWithCustomEndpoint = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic' } + })); + const requests = spec.buildRequests(bidRequestsWithCustomEndpoint, bidderRequest); - expect(request.url).to.equal('https://global.iprom.net/programmatic'); - expect(request.ortb).to.equal(true); - expect(request.data).to.be.an('object'); - expect(request.data.imp).to.be.an('array').with.lengthOf(1); - expect(request.data.bids).to.be.undefined; + expect(requests[0].url).to.equal('https://global.iprom.net/programmatic'); + expect(requests[0].ortb).to.be.undefined; + expect(requests[0].data).to.be.a('string'); }); - it('should ignore invalid custom endpoint and use default endpoint', function () { - config.setConfig({ - iprom: { - endpoint: 'not-a-valid-url' - } - }); - const request = spec.buildRequests(bidRequests, bidderRequest); + it('should use ORTB format when ortb param is true', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].url).to.equal('https://core.iprom.net/programmatic'); + expect(requests[0].ortb).to.equal(true); + expect(requests[0].data).to.be.an('object'); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.bids).to.be.undefined; + }); - expect(request.url).to.equal('https://core.iprom.net/programmatic'); - expect(request.ortb).to.be.undefined; - expect(request.data).to.be.a('string'); + it('should use custom endpoint with ORTB format when both endpoint and ortb params are set', function () { + const bidRequestsWithBoth = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic', ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithBoth, bidderRequest); + + expect(requests[0].url).to.equal('https://global.iprom.net/programmatic'); + expect(requests[0].ortb).to.equal(true); + expect(requests[0].data).to.be.an('object'); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.bids).to.be.undefined; }); it('should include schain in ORTB request when present in bidderRequest.ortb2', function () { @@ -163,11 +232,10 @@ describe('iPROM Adapter', function () { hp: 1 }] }; - config.setConfig({ - iprom: { - endpoint: 'https://global.iprom.net/programmatic' - } - }); + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic', ortb: true } + })); const bidderRequestWithGlobalSchain = { ...bidderRequest, ortb2: { @@ -179,14 +247,76 @@ describe('iPROM Adapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequestWithGlobalSchain); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequestWithGlobalSchain); - expect(request.data.source.ext.schain).to.deep.equal(schain); + expect(requests[0].data.source.ext.schain).to.deep.equal(schain); + }); + + it('should group bids with different endpoints into separate requests', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, endpoint: 'https://endpoint1.iprom.net/programmatic' } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, endpoint: 'https://endpoint2.iprom.net/programmatic' } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(2); + const urls = requests.map(r => r.url); + expect(urls).to.include('https://endpoint1.iprom.net/programmatic'); + expect(urls).to.include('https://endpoint2.iprom.net/programmatic'); + }); + + it('should group bids with same endpoint but different ortb into separate requests', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, ortb: true } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, ortb: false } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(2); + + const ortbRequest = requests.find(r => r.ortb === true); + const legacyRequest = requests.find(r => r.ortb === undefined); + + expect(ortbRequest).to.exist; + expect(ortbRequest.data).to.be.an('object'); + expect(ortbRequest.data.imp).to.be.an('array').with.lengthOf(1); + + expect(legacyRequest).to.exist; + expect(legacyRequest.data).to.be.a('string'); + }); + + it('should group bids with same endpoint and ortb into a single request', function () { + const bidRequest1 = { + ...bidRequests[0], + bidId: 'bid1', + params: { ...bidRequests[0].params, ortb: true } + }; + const bidRequest2 = { + ...bidRequests[0], + bidId: 'bid2', + params: { ...bidRequests[0].params, ortb: true } + }; + const requests = spec.buildRequests([bidRequest1, bidRequest2], bidderRequest); + + expect(requests).to.be.an('array').with.lengthOf(1); + expect(requests[0].data.imp).to.be.an('array').with.lengthOf(2); }); it('should add only selected referer info', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.referer).to.deep.equal({ reachedTop: true, @@ -220,8 +350,8 @@ describe('iPROM Adapter', function () { }); it('should add adapter version', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.version).to.exist; }); @@ -235,8 +365,8 @@ describe('iPROM Adapter', function () { addtlConsent: 'addtl-consent' } }; - const request = spec.buildRequests(bidRequests, bidderRequestWithTcf); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequestWithTcf); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.tcf).to.deep.equal({ consentString: 'consent-string', @@ -282,8 +412,8 @@ describe('iPROM Adapter', function () { } } }; - const request = spec.buildRequests(bidRequests, bidderRequestWithSchain); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequestWithSchain); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.schain).to.deep.equal(schain); }); @@ -311,8 +441,8 @@ describe('iPROM Adapter', function () { } } }; - const request = spec.buildRequests(bidRequests, bidderRequestWithFpdAndSchain); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequestWithFpdAndSchain); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.schain).to.deep.equal(schain); expect(requestparse.firstPartyData).to.be.undefined; @@ -335,8 +465,8 @@ describe('iPROM Adapter', function () { ...bidderRequest, ortb2: firstPartyData }; - const request = spec.buildRequests(bidRequests, bidderRequestWithFpd); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequestWithFpd); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.firstPartyData).to.deep.equal({ user: { @@ -349,8 +479,8 @@ describe('iPROM Adapter', function () { }); it('should contain id and dimension', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - const requestparse = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(requests[0].data); expect(requestparse.bids[0].params.id).to.equal('1234'); expect(requestparse.bids[0].params.dimension).to.equal('300x250'); @@ -371,8 +501,8 @@ describe('iPROM Adapter', function () { } ]}; - const request = spec.buildRequests(bidRequests, bidderRequest); - const bids = spec.interpretResponse(serverResponse, request); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, requests[0]); expect(bids).to.be.lengthOf(1); expect(bids[0].requestId).to.equal('29a72b151f7bd3'); @@ -398,8 +528,8 @@ describe('iPROM Adapter', function () { }] }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const bids = spec.interpretResponse(serverResponse, request); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, requests[0]); expect(bids).to.be.lengthOf(1); expect(bids[0].netRevenue).to.equal(false); @@ -407,14 +537,13 @@ describe('iPROM Adapter', function () { expect(bids[0].currency).to.equal('USD'); }); - it('should parse OpenRTB response when custom endpoint is used', function () { - config.setConfig({ - iprom: { - endpoint: 'https://global.iprom.net/programmatic' - } - }); - const request = spec.buildRequests(bidRequests, bidderRequest); - const impId = request.data.imp[0].id; + it('should parse OpenRTB response when ortb param is true', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, endpoint: 'https://global.iprom.net/programmatic', ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + const impId = requests[0].data.imp[0].id; const serverResponse = { body: { cur: 'EUR', @@ -433,7 +562,7 @@ describe('iPROM Adapter', function () { } }; - const bids = spec.interpretResponse(serverResponse, request); + const bids = spec.interpretResponse(serverResponse, requests[0]); expect(bids).to.be.lengthOf(1); expect(bids[0].requestId).to.equal('29a72b151f7bd3'); @@ -447,8 +576,8 @@ describe('iPROM Adapter', function () { body: {} }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const bids = spec.interpretResponse(malformedServerResponse, request); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(malformedServerResponse, requests[0]); expect(bids).to.be.lengthOf(0); }); @@ -458,8 +587,8 @@ describe('iPROM Adapter', function () { body: [] }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const bids = spec.interpretResponse(emptyServerResponse, request); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(emptyServerResponse, requests[0]); expect(bids).to.be.lengthOf(0); }); From a4e9b6ca2db1b3fd26595155fd5e4d62fdce3eb5 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Fri, 6 Mar 2026 15:29:06 +0100 Subject: [PATCH 07/12] iPROM Adapter: add referer info to ORTB request site.ext Add reachedTop, numIframes, and stack from refererInfo to site.ext in the ORTB payload via a custom request processor on ortbConverter. Co-Authored-By: Claude Opus 4.6 --- modules/ipromBidAdapter.js | 18 +++++++++++++++ test/spec/modules/ipromBidAdapter_spec.js | 28 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 38e09cda5be..91b58512a0d 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -13,6 +13,24 @@ const converter = ortbConverter({ context: { netRevenue: DEFAULT_NETREVENUE, ttl: DEFAULT_TTL + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const refererInfo = bidderRequest?.refererInfo; + + if (refererInfo) { + const ext = {}; + if (refererInfo.reachedTop != null) ext.reachedTop = refererInfo.reachedTop; + if (refererInfo.numIframes != null) ext.numIframes = refererInfo.numIframes; + if (refererInfo.stack != null) ext.stack = refererInfo.stack; + + if (Object.keys(ext).length) { + if (!request.site) request.site = {}; + request.site.ext = Object.assign({}, request.site.ext, ext); + } + } + + return request; } }); diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index a314e98eca5..8085f9124b8 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -252,6 +252,34 @@ describe('iPROM Adapter', function () { expect(requests[0].data.source.ext.schain).to.deep.equal(schain); }); + it('should include referer info in ORTB request site.ext when present', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.site.ext.reachedTop).to.equal(true); + expect(requests[0].data.site.ext.numIframes).to.equal(1); + expect(requests[0].data.site.ext.stack).to.deep.equal([ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ]); + }); + + it('should not add referer ext fields to ORTB request when refererInfo is missing', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const bidderRequestWithoutReferer = { ...bidderRequest, refererInfo: undefined }; + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequestWithoutReferer); + + expect(requests[0].data.site?.ext?.reachedTop).to.be.undefined; + expect(requests[0].data.site?.ext?.numIframes).to.be.undefined; + expect(requests[0].data.site?.ext?.stack).to.be.undefined; + }); + it('should group bids with different endpoints into separate requests', function () { const bidRequest1 = { ...bidRequests[0], From 0d9024f70217ae77b6f6ff791852f00ff7d05c55 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Mon, 9 Mar 2026 11:57:48 +0100 Subject: [PATCH 08/12] Minor fixes --- modules/ipromBidAdapter.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 91b58512a0d..4e9a03c9388 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -30,6 +30,9 @@ const converter = ortbConverter({ } } + if (!request.ext) request.ext = {}; + request.ext.adapterVersion = VERSION; + return request; } }); @@ -50,8 +53,12 @@ function isValidEndpointUrl(endpoint) { } function resolveEndpoint(endpoint) { - if (typeof endpoint === 'string' && isValidEndpointUrl(endpoint)) { - return endpoint; + if (typeof endpoint === 'string') { + if (isValidEndpointUrl(endpoint)) { + return endpoint; + } else { + logWarn(`${BIDDER_CODE}: Endpoint ${endpoint} is not a valid URL, using default endpoint`); + } } return ENDPOINT_URL; @@ -73,10 +80,10 @@ function extractReferer(refererInfo) { logMissingFields('referer', missingRefererFields); const referer = { - reachedTop: refererInfo.reachedTop, - referer: refererUrl, - numIframes: refererInfo.numIframes, - stack: refererInfo.stack + reachedTop: refererInfo.reachedTop ?? null, + referer: refererUrl ?? null, + numIframes: refererInfo.numIframes ?? null, + stack: refererInfo.stack ?? null }; return Object.values(referer).some(value => value != null) ? referer : null; @@ -88,9 +95,9 @@ function extractTcf(gdprConsent) { } const tcf = { - consentString: gdprConsent.consentString, - gdprApplies: gdprConsent.gdprApplies, - addtlConsent: gdprConsent.addtlConsent + consentString: gdprConsent.consentString ?? null, + gdprApplies: gdprConsent.gdprApplies ?? null, + addtlConsent: gdprConsent.addtlConsent ?? null }; const missingTcfFields = []; From 542f4bf2ea701482f411316725e76aa00d64f286 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Mon, 9 Mar 2026 13:44:14 +0100 Subject: [PATCH 09/12] Removed tests that were testing internal implementaion --- test/spec/modules/ipromBidAdapter_spec.js | 37 ----------------------- 1 file changed, 37 deletions(-) diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 8085f9124b8..44e82d38bfe 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -1,6 +1,4 @@ import {expect} from 'chai'; -import sinon from 'sinon'; -import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; import {spec} from 'modules/ipromBidAdapter.js'; @@ -359,24 +357,6 @@ describe('iPROM Adapter', function () { expect(requestparse.referer.legacy).to.be.undefined; }); - it('should warn if referer fields are missing', function () { - const warnSpy = sinon.spy(utils, 'logWarn'); - - const bidderRequestWithMissingRefererFields = { - ...bidderRequest, - refererInfo: { - reachedTop: true - } - }; - - spec.buildRequests(bidRequests, bidderRequestWithMissingRefererFields); - - expect(warnSpy.calledOnce).to.equal(true); - expect(warnSpy.firstCall.args[0]).to.equal('iprom: Missing referer fields: referer, numIframes, stack'); - - warnSpy.restore(); - }); - it('should add adapter version', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(requests[0].data); @@ -403,23 +383,6 @@ describe('iPROM Adapter', function () { }); }); - it('should warn if TCF fields are missing', function () { - const warnSpy = sinon.spy(utils, 'logWarn'); - const bidderRequestWithIncompleteTcf = { - ...bidderRequest, - gdprConsent: { - consentString: 'consent-string' - } - }; - - spec.buildRequests(bidRequests, bidderRequestWithIncompleteTcf); - - expect(warnSpy.calledOnce).to.equal(true); - expect(warnSpy.firstCall.args[0]).to.equal('iprom: Missing tcf fields: gdprApplies, addtlConsent'); - - warnSpy.restore(); - }); - it('should add schain data', function () { const schain = { ver: '1.0', From 04c3ba2b4585cd7cc2c619057d91e4ff87f815fd Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Mon, 9 Mar 2026 13:44:44 +0100 Subject: [PATCH 10/12] Hardened endpoint param validation. --- modules/ipromBidAdapter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 4e9a03c9388..3ce393e0e61 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -46,7 +46,7 @@ function logMissingFields(scope, missingFields) { function isValidEndpointUrl(endpoint) { try { const parsedEndpoint = new URL(endpoint); - return parsedEndpoint.protocol === 'http:' || parsedEndpoint.protocol === 'https:'; + return parsedEndpoint.protocol === 'https:'; } catch (e) { return false; } @@ -57,7 +57,7 @@ function resolveEndpoint(endpoint) { if (isValidEndpointUrl(endpoint)) { return endpoint; } else { - logWarn(`${BIDDER_CODE}: Endpoint ${endpoint} is not a valid URL, using default endpoint`); + logWarn(`${BIDDER_CODE}: Endpoint ${endpoint} is not a valid HTTPS URL, using default endpoint`); } } @@ -207,7 +207,7 @@ export const spec = { } if (params.endpoint !== undefined && !isValidEndpointUrl(params.endpoint)) { - logError(`${bidderName}: Parameter 'endpoint' needs to be a valid URL`); + logError(`${bidderName}: Parameter 'endpoint' needs to be a valid HTTPS URL`); return false; } From 94508d49089d1571eab75b92c0b4b2e429ac0a82 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Tue, 10 Mar 2026 11:02:45 +0100 Subject: [PATCH 11/12] feat: Sendin id and dimension bid params in ortb request paylod. --- modules/ipromBidAdapter.js | 13 ++++++++++- test/spec/modules/ipromBidAdapter_spec.js | 28 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index 3ce393e0e61..e040c14b2d0 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -1,4 +1,4 @@ -import { deepClone, logError, logWarn } from '../src/utils.js'; +import { deepClone, deepSetValue, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; @@ -14,6 +14,17 @@ const converter = ortbConverter({ netRevenue: DEFAULT_NETREVENUE, ttl: DEFAULT_TTL }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { id, dimension } = bidRequest.params || {}; + + deepSetValue(imp, 'ext.bidder.id', id); + if (dimension != null) { + deepSetValue(imp, 'ext.bidder.dimension', dimension); + } + + return imp; + }, request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); const refererInfo = bidderRequest?.refererInfo; diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index 44e82d38bfe..24ceba29beb 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -206,6 +206,34 @@ describe('iPROM Adapter', function () { expect(requests[0].data.bids).to.be.undefined; }); + it('should include id and dimension in imp.ext.bidder for ORTB requests', function () { + const bidRequestsWithOrtb = bidRequests.map(bid => ({ + ...bid, + params: { ...bid.params, ortb: true } + })); + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.imp[0].ext.bidder).to.deep.equal({ + id: '1234', + dimension: '300x250' + }); + }); + + it('should include only id in imp.ext.bidder when dimension is not set', function () { + const bidRequestsWithOrtb = [{ + ...bidRequests[0], + params: { + id: '1234', + ortb: true + } + }]; + const requests = spec.buildRequests(bidRequestsWithOrtb, bidderRequest); + + expect(requests[0].data.imp[0].ext.bidder).to.deep.equal({ + id: '1234' + }); + }); + it('should use custom endpoint with ORTB format when both endpoint and ortb params are set', function () { const bidRequestsWithBoth = bidRequests.map(bid => ({ ...bid, From 34c6897550a8be0151a8c42bc56701c7d5efa220 Mon Sep 17 00:00:00 2001 From: oskarcokl Date: Thu, 19 Mar 2026 14:54:14 +0100 Subject: [PATCH 12/12] fix: Lint errors. --- modules/ipromBidAdapter.js | 2 +- test/spec/modules/ipromBidAdapter_spec.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index e040c14b2d0..6a54eabd6aa 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -264,7 +264,7 @@ export const spec = { interpretResponse: function (serverResponse, request) { if (request?.ortb) { - return converter.fromORTB({response: serverResponse?.body, request: request.data}).bids ?? []; + return converter.fromORTB({ response: serverResponse?.body, request: request.data }).bids ?? []; } const bids = Array.isArray(serverResponse?.body) ? serverResponse.body : []; diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js index acb4441ba51..d162cbbf84e 100644 --- a/test/spec/modules/ipromBidAdapter_spec.js +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; -import {config} from 'src/config.js'; -import {spec} from 'modules/ipromBidAdapter.js'; +import { expect } from 'chai'; +import { config } from 'src/config.js'; +import { spec } from 'modules/ipromBidAdapter.js'; describe('iPROM Adapter', function () { let bidRequests; @@ -476,7 +476,7 @@ describe('iPROM Adapter', function () { user: { data: [{ name: 'taxonomy', - segment: [{id: 'segment-id'}] + segment: [{ id: 'segment-id' }] }] } }; @@ -491,7 +491,7 @@ describe('iPROM Adapter', function () { user: { data: [{ name: 'taxonomy', - segment: [{id: 'segment-id'}] + segment: [{ id: 'segment-id' }] }] } });