From a09f9b96e2df5ffe362ba18f6dc8628245a0db19 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Wed, 9 Jul 2025 12:04:21 +0300 Subject: [PATCH 01/10] adding programmaticxOrtbBidAdapter --- modules/programmaticxOrtbBidAdapter.js | 39 ++++++++++++++++++++++++++ modules/programmaticxOrtbBidAdapter.md | 35 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 modules/programmaticxOrtbBidAdapter.js create mode 100644 modules/programmaticxOrtbBidAdapter.md diff --git a/modules/programmaticxOrtbBidAdapter.js b/modules/programmaticxOrtbBidAdapter.js new file mode 100644 index 00000000000..79b21b27c9e --- /dev/null +++ b/modules/programmaticxOrtbBidAdapter.js @@ -0,0 +1,39 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import { + createBuildRequestsFn, + createInterpretResponseFn, + createUserSyncGetter, + isBidRequestValid +} from '../libraries/vidazooUtils/bidderUtils.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'programmaticx'; +const BIDDER_VERSION = '1.0.0'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.programmaticx.ai`; +} + +const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); + +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); + +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.programmaticx.ai/api/sync/iframe', + imageSyncUrl: 'https://sync.programmaticx.ai/api/sync/image' +}); + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/programmaticxOrtbBidAdapter.md b/modules/programmaticxOrtbBidAdapter.md new file mode 100644 index 00000000000..fd4cd25a5bd --- /dev/null +++ b/modules/programmaticxOrtbBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Programmaticx PX Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** pxteam@programmaticx.ai + +# Description + +Module that connects to Programmaticx's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'programmaticx', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` From a59d20d1ceb7a1c13fe1238427292fd2305e84fc Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Thu, 10 Jul 2025 14:06:12 +0300 Subject: [PATCH 02/10] adding programmaticxOrtbBidAdapter tests --- package-lock.json | 117 --- .../programmaticxOrtbBidAdapter_spec.js | 701 ++++++++++++++++++ 2 files changed, 701 insertions(+), 117 deletions(-) create mode 100644 test/spec/modules/programmaticxOrtbBidAdapter_spec.js diff --git a/package-lock.json b/package-lock.json index ed26b8bc91b..fbd36d4f746 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,7 +85,6 @@ "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "^0.0.32", "karma-webpack": "^5.0.0", - "lcov-result-merger": "^5.0.1", "lodash": "^4.17.21", "merge-stream": "^2.0.0", "mocha": "^10.7.3", @@ -13246,122 +13245,6 @@ "lcov-parse": "bin/cli.js" } }, - "node_modules/lcov-result-merger": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/lcov-result-merger/-/lcov-result-merger-5.0.1.tgz", - "integrity": "sha512-i53RjTYfqbHgerqGtuJjDfARDU340zNxXrJudQZU3o8ak9rrx8FDQUKf38Cjm6MtbqonqiDFmoKuUe++uZbvOg==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.11", - "yargs": "^16.2.0" - }, - "bin": { - "lcov-result-merger": "bin/lcov-result-merger.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/lcov-result-merger/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/lcov-result-merger/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/lcov-result-merger/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/lcov-result-merger/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/lcov-result-merger/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lcov-result-merger/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lcov-result-merger/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lcov-result-merger/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/lead": { "version": "4.0.0", "dev": true, diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js new file mode 100644 index 00000000000..e1c443142c8 --- /dev/null +++ b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js @@ -0,0 +1,701 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + storage +} from 'modules/programmaticxOrtbBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; +import { + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from '../../../libraries/vidazooUtils/bidderUtils.js'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const ORTB2_DEVICE = { + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + ext: {fiftyonedegrees_deviceId: '17595-133085-133468-18092'}, +}; + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'site': { + 'content': { + 'language': 'en' + } + }, + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7], + 'coppa': 0 + }, + 'device': ORTB2_DEVICE, + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['programmaticx.ai'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('programmaticxOrtbBidAdapter', function () { + before(() => config.resetConfig()); + after(() => config.resetConfig()); + + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticx: { + storageAllowed: true + } + }; + sandbox = sinon.createSandbox(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + device: ORTB2_DEVICE, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + device: ORTB2_DEVICE, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', + 'type': 'image' + }]); + }); + + it('should have valid user sync with coppa 1 on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&coppa=1&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['programmaticx.ai'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['programmaticx.ai'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['programmaticx.ai'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticx: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(storage, key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(storage, key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(storage, key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticx: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem(storage, 'myKey', 2020); + const {value, created} = getStorageItem(storage, 'myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem(storage, 'myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); From fe1c62619353f598234712102b1b1dc6ed6933d4 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Thu, 10 Jul 2025 16:34:44 +0300 Subject: [PATCH 03/10] reviewing TEST_ID_SYSTEMS --- test/spec/modules/programmaticxOrtbBidAdapter_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js index e1c443142c8..8df7748d584 100644 --- a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js +++ b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js @@ -20,7 +20,7 @@ import { getUniqueDealId, } from '../../../libraries/vidazooUtils/bidderUtils.js'; -export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; +export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'netId', 'tdid', 'pubProvidedId', 'intentIqId', 'liveIntentId']; const SUB_DOMAIN = 'exchange'; From 2e0ad04463f85519ba6ee005d5bee138f648007e Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Sun, 13 Jul 2025 09:13:21 +0300 Subject: [PATCH 04/10] adding required test; fixing code --- modules/programmaticxOrtbBidAdapter.js | 2 +- test/spec/modules/programmaticxOrtbBidAdapter_spec.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/programmaticxOrtbBidAdapter.js b/modules/programmaticxOrtbBidAdapter.js index 79b21b27c9e..289fa6b760d 100644 --- a/modules/programmaticxOrtbBidAdapter.js +++ b/modules/programmaticxOrtbBidAdapter.js @@ -9,7 +9,7 @@ import { } from '../libraries/vidazooUtils/bidderUtils.js'; const DEFAULT_SUB_DOMAIN = 'exchange'; -const BIDDER_CODE = 'programmaticx'; +const BIDDER_CODE = 'programmaticxortb'; const BIDDER_VERSION = '1.0.0'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js index 8df7748d584..1f0f853fb58 100644 --- a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js +++ b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js @@ -698,4 +698,10 @@ describe('programmaticxOrtbBidAdapter', function () { expect(parsed).to.be.equal(value); }); }); + describe('createDomain test', function() { + it('should return correct domain', function () { + const responses = createDomain(); + expect(responses).to.be.equal('https://exchange.programmaticx.ai'); + }); + }) }); From 52397ecbbe256e9eb178b6e4789401efe37c6bd1 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Sun, 13 Jul 2025 12:31:08 +0300 Subject: [PATCH 05/10] adding minor changes --- modules/programmaticxOrtbBidAdapter.md | 4 ++-- test/spec/modules/programmaticxOrtbBidAdapter_spec.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/programmaticxOrtbBidAdapter.md b/modules/programmaticxOrtbBidAdapter.md index fd4cd25a5bd..dd0bc29cd37 100644 --- a/modules/programmaticxOrtbBidAdapter.md +++ b/modules/programmaticxOrtbBidAdapter.md @@ -8,7 +8,7 @@ # Description -Module that connects to Programmaticx's demand sources. +Module that connects to Programmaticx's Open RTB demand sources. # Test Parameters ```js @@ -18,7 +18,7 @@ var adUnits = [ sizes: [[300, 250]], bids: [ { - bidder: 'programmaticx', + bidder: 'programmaticxortb', params: { cId: '562524b21b1c1f08117fc7f9', pId: '59ac17c192832d0011283fe3', diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js index 1f0f853fb58..ffe9f731b26 100644 --- a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js +++ b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js @@ -698,6 +698,7 @@ describe('programmaticxOrtbBidAdapter', function () { expect(parsed).to.be.equal(value); }); }); + describe('createDomain test', function() { it('should return correct domain', function () { const responses = createDomain(); From 78586ce2d4e2295c289d6ab0ec9d61a1347368fb Mon Sep 17 00:00:00 2001 From: saar120 Date: Sun, 13 Jul 2025 12:50:39 +0300 Subject: [PATCH 06/10] fix wrong bidder code in tests --- test/spec/modules/programmaticxOrtbBidAdapter_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js index ffe9f731b26..11d490fe877 100644 --- a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js +++ b/test/spec/modules/programmaticxOrtbBidAdapter_spec.js @@ -272,7 +272,7 @@ describe('programmaticxOrtbBidAdapter', function () { let sandbox; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticx: { + programmaticxortb: { storageAllowed: true } }; @@ -620,7 +620,7 @@ describe('programmaticxOrtbBidAdapter', function () { describe('unique deal id', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticx: { + programmaticxortb: { storageAllowed: true } }; @@ -655,7 +655,7 @@ describe('programmaticxOrtbBidAdapter', function () { describe('storage utils', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticx: { + programmaticxortb: { storageAllowed: true } }; From 97870b845934ad3274b911565b2d3fa950dd5da9 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Tue, 15 Jul 2025 17:37:30 +0300 Subject: [PATCH 07/10] change adapter --- ...grammaticxOrtbBidAdapter.js => pxBidAdapter.js} | 2 +- ...grammaticxOrtbBidAdapter.md => pxBidAdapter.md} | 6 +++--- ...OrtbBidAdapter_spec.js => pxBidAdapter_spec.js} | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename modules/{programmaticxOrtbBidAdapter.js => pxBidAdapter.js} (96%) rename modules/{programmaticxOrtbBidAdapter.md => pxBidAdapter.md} (75%) rename test/spec/modules/{programmaticxOrtbBidAdapter_spec.js => pxBidAdapter_spec.js} (98%) diff --git a/modules/programmaticxOrtbBidAdapter.js b/modules/pxBidAdapter.js similarity index 96% rename from modules/programmaticxOrtbBidAdapter.js rename to modules/pxBidAdapter.js index 289fa6b760d..9bc49da1fde 100644 --- a/modules/programmaticxOrtbBidAdapter.js +++ b/modules/pxBidAdapter.js @@ -9,7 +9,7 @@ import { } from '../libraries/vidazooUtils/bidderUtils.js'; const DEFAULT_SUB_DOMAIN = 'exchange'; -const BIDDER_CODE = 'programmaticxortb'; +const BIDDER_CODE = 'px'; const BIDDER_VERSION = '1.0.0'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); diff --git a/modules/programmaticxOrtbBidAdapter.md b/modules/pxBidAdapter.md similarity index 75% rename from modules/programmaticxOrtbBidAdapter.md rename to modules/pxBidAdapter.md index dd0bc29cd37..bbd5b47c647 100644 --- a/modules/programmaticxOrtbBidAdapter.md +++ b/modules/pxBidAdapter.md @@ -1,6 +1,6 @@ # Overview -**Module Name:** Programmaticx PX Bidder Adapter +**Module Name:** PX Bidder Adapter **Module Type:** Bidder Adapter @@ -8,7 +8,7 @@ # Description -Module that connects to Programmaticx's Open RTB demand sources. +Module that connects to PX's Open RTB demand sources. # Test Parameters ```js @@ -18,7 +18,7 @@ var adUnits = [ sizes: [[300, 250]], bids: [ { - bidder: 'programmaticxortb', + bidder: 'px', params: { cId: '562524b21b1c1f08117fc7f9', pId: '59ac17c192832d0011283fe3', diff --git a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js b/test/spec/modules/pxBidAdapter_spec.js similarity index 98% rename from test/spec/modules/programmaticxOrtbBidAdapter_spec.js rename to test/spec/modules/pxBidAdapter_spec.js index 11d490fe877..68050e830af 100644 --- a/test/spec/modules/programmaticxOrtbBidAdapter_spec.js +++ b/test/spec/modules/pxBidAdapter_spec.js @@ -3,12 +3,12 @@ import { spec as adapter, createDomain, storage -} from 'modules/programmaticxOrtbBidAdapter'; +} from 'modules/pxBidAdapter'; import * as utils from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; -import {BANNER, VIDEO} from '../../../src/mediaTypes'; -import {config} from '../../../src/config'; +import {BANNER, VIDEO} from '../../../src/mediaTypes.js' +import {config} from '../../../src/config.js'; import { hashCode, extractPID, @@ -207,7 +207,7 @@ function getTopWindowQueryParams() { } } -describe('programmaticxOrtbBidAdapter', function () { +describe('pxBidAdapter', function () { before(() => config.resetConfig()); after(() => config.resetConfig()); @@ -272,7 +272,7 @@ describe('programmaticxOrtbBidAdapter', function () { let sandbox; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticxortb: { + px: { storageAllowed: true } }; @@ -620,7 +620,7 @@ describe('programmaticxOrtbBidAdapter', function () { describe('unique deal id', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticxortb: { + px: { storageAllowed: true } }; @@ -655,7 +655,7 @@ describe('programmaticxOrtbBidAdapter', function () { describe('storage utils', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - programmaticxortb: { + px: { storageAllowed: true } }; From 3a0141b4cd9862c5ee5608f660fb1147c3f83701 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Wed, 16 Jul 2025 08:40:40 +0300 Subject: [PATCH 08/10] change adapter --- modules/{pxBidAdapter.js => progxBidAdapter.js} | 2 +- modules/{pxBidAdapter.md => progxBidAdapter.md} | 6 +++--- .../{pxBidAdapter_spec.js => progxBidAdapter_spec.js} | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) rename modules/{pxBidAdapter.js => progxBidAdapter.js} (97%) rename modules/{pxBidAdapter.md => progxBidAdapter.md} (79%) rename test/spec/modules/{pxBidAdapter_spec.js => progxBidAdapter_spec.js} (99%) diff --git a/modules/pxBidAdapter.js b/modules/progxBidAdapter.js similarity index 97% rename from modules/pxBidAdapter.js rename to modules/progxBidAdapter.js index 9bc49da1fde..dae94f5c215 100644 --- a/modules/pxBidAdapter.js +++ b/modules/progxBidAdapter.js @@ -9,7 +9,7 @@ import { } from '../libraries/vidazooUtils/bidderUtils.js'; const DEFAULT_SUB_DOMAIN = 'exchange'; -const BIDDER_CODE = 'px'; +const BIDDER_CODE = 'progx'; const BIDDER_VERSION = '1.0.0'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); diff --git a/modules/pxBidAdapter.md b/modules/progxBidAdapter.md similarity index 79% rename from modules/pxBidAdapter.md rename to modules/progxBidAdapter.md index bbd5b47c647..9f2538baca0 100644 --- a/modules/pxBidAdapter.md +++ b/modules/progxBidAdapter.md @@ -1,6 +1,6 @@ # Overview -**Module Name:** PX Bidder Adapter +**Module Name:** ProgX Bidder Adapter **Module Type:** Bidder Adapter @@ -8,7 +8,7 @@ # Description -Module that connects to PX's Open RTB demand sources. +Module that connects to ProgX's Open RTB demand sources. # Test Parameters ```js @@ -18,7 +18,7 @@ var adUnits = [ sizes: [[300, 250]], bids: [ { - bidder: 'px', + bidder: 'progx', params: { cId: '562524b21b1c1f08117fc7f9', pId: '59ac17c192832d0011283fe3', diff --git a/test/spec/modules/pxBidAdapter_spec.js b/test/spec/modules/progxBidAdapter_spec.js similarity index 99% rename from test/spec/modules/pxBidAdapter_spec.js rename to test/spec/modules/progxBidAdapter_spec.js index 68050e830af..bba762c9230 100644 --- a/test/spec/modules/pxBidAdapter_spec.js +++ b/test/spec/modules/progxBidAdapter_spec.js @@ -3,7 +3,7 @@ import { spec as adapter, createDomain, storage -} from 'modules/pxBidAdapter'; +} from 'modules/progxBidAdapter'; import * as utils from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; @@ -207,7 +207,7 @@ function getTopWindowQueryParams() { } } -describe('pxBidAdapter', function () { +describe('progxBidAdapter', function () { before(() => config.resetConfig()); after(() => config.resetConfig()); @@ -272,7 +272,7 @@ describe('pxBidAdapter', function () { let sandbox; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - px: { + progx: { storageAllowed: true } }; @@ -620,7 +620,7 @@ describe('pxBidAdapter', function () { describe('unique deal id', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - px: { + progx: { storageAllowed: true } }; @@ -655,7 +655,7 @@ describe('pxBidAdapter', function () { describe('storage utils', function () { before(function () { $$PREBID_GLOBAL$$.bidderSettings = { - px: { + progx: { storageAllowed: true } }; From 7312b3ce1d3037d891cd36451998e24665e16810 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Mon, 28 Jul 2025 12:22:48 +0300 Subject: [PATCH 09/10] replacing old programmaticX adapter with new code --- modules/programmaticXBidAdapter.js | 41 +- modules/programmaticXBidAdapter.md | 98 +- modules/progxBidAdapter.js | 39 - modules/progxBidAdapter.md | 35 - .../modules/programmaticXBidAdapter_spec.js | 1136 ++++++++++------- test/spec/modules/progxBidAdapter_spec.js | 708 ---------- 6 files changed, 721 insertions(+), 1336 deletions(-) delete mode 100644 modules/progxBidAdapter.js delete mode 100644 modules/progxBidAdapter.md delete mode 100644 test/spec/modules/progxBidAdapter_spec.js diff --git a/modules/programmaticXBidAdapter.js b/modules/programmaticXBidAdapter.js index f6a50789380..1f0333b9a3c 100644 --- a/modules/programmaticXBidAdapter.js +++ b/modules/programmaticXBidAdapter.js @@ -1,21 +1,42 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import { + createBuildRequestsFn, + createInterpretResponseFn, + createUserSyncGetter, + isBidRequestValid +} from '../libraries/vidazooUtils/bidderUtils.js'; +const DEFAULT_SUB_DOMAIN = 'exchange'; const BIDDER_CODE = 'programmaticX'; +const BIDDER_VERSION = '1.0.0'; const GVLID = 1344; -const AD_URL = 'https://us-east.progrtb.com/pbjs'; -const SYNC_URL = 'https://sync.progrtb.com'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.programmaticx.ai`; +} + +const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); + +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); + +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.programmaticx.ai/api/sync/iframe', + imageSyncUrl: 'https://sync.programmaticx.ai/api/sync/image' +}); export const spec = { code: BIDDER_CODE, + version: BIDDER_VERSION, gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - - isBidRequestValid: isBidRequestValid(), - buildRequests: buildRequests(AD_URL), + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, interpretResponse, - getUserSyncs: getUserSyncs(SYNC_URL) + getUserSyncs }; registerBidder(spec); diff --git a/modules/programmaticXBidAdapter.md b/modules/programmaticXBidAdapter.md index ad1ab316cde..89f87dff8b7 100644 --- a/modules/programmaticXBidAdapter.md +++ b/modules/programmaticXBidAdapter.md @@ -1,79 +1,35 @@ # Overview -``` -Module Name: ProgrammaticX Bidder Adapter -Module Type: ProgrammaticX Bidder Adapter -Maintainer: pxteam@programmaticx.ai -``` +**Module Name:** ProgrammaticX Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** pxteam@programmaticx.ai # Description -Connects to ProgrammaticX Bidder exchange for bids. -ProgrammaticX Bidder bid adapter supports Banner, Video (instream and outstream) and Native. +Module that connects to ProgrammaticX's Open RTB demand sources. # Test Parameters -``` - var adUnits = [ - // Will return static test banner - { - code: 'adunit1', - mediaTypes: { - banner: { - sizes: [ [300, 250], [320, 50] ], - } - }, - bids: [ - { - bidder: 'programmaticX', - params: { - placementId: 'testBanner', - } - } - ] - }, - { - code: 'addunit2', - mediaTypes: { - video: { - playerSize: [ [640, 480] ], - context: 'instream', - minduration: 5, - maxduration: 60, - } - }, - bids: [ - { - bidder: 'programmaticX', - params: { - placementId: 'testVideo', - } - } - ] - }, - { - code: 'addunit3', - mediaTypes: { - native: { - title: { - required: true - }, - body: { - required: true - }, - icon: { - required: true, - size: [64, 64] - } - } - }, - bids: [ - { - bidder: 'programmaticX', - params: { - placementId: 'testNative', - } - } - ] - } - ]; +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'programmaticX', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; ``` diff --git a/modules/progxBidAdapter.js b/modules/progxBidAdapter.js deleted file mode 100644 index dae94f5c215..00000000000 --- a/modules/progxBidAdapter.js +++ /dev/null @@ -1,39 +0,0 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getStorageManager} from '../src/storageManager.js'; -import { - createBuildRequestsFn, - createInterpretResponseFn, - createUserSyncGetter, - isBidRequestValid -} from '../libraries/vidazooUtils/bidderUtils.js'; - -const DEFAULT_SUB_DOMAIN = 'exchange'; -const BIDDER_CODE = 'progx'; -const BIDDER_VERSION = '1.0.0'; -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { - return `https://${subDomain}.programmaticx.ai`; -} - -const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); - -const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); - -const getUserSyncs = createUserSyncGetter({ - iframeSyncUrl: 'https://sync.programmaticx.ai/api/sync/iframe', - imageSyncUrl: 'https://sync.programmaticx.ai/api/sync/image' -}); - -export const spec = { - code: BIDDER_CODE, - version: BIDDER_VERSION, - supportedMediaTypes: [BANNER, VIDEO], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs -}; - -registerBidder(spec); diff --git a/modules/progxBidAdapter.md b/modules/progxBidAdapter.md deleted file mode 100644 index 9f2538baca0..00000000000 --- a/modules/progxBidAdapter.md +++ /dev/null @@ -1,35 +0,0 @@ -# Overview - -**Module Name:** ProgX Bidder Adapter - -**Module Type:** Bidder Adapter - -**Maintainer:** pxteam@programmaticx.ai - -# Description - -Module that connects to ProgX's Open RTB demand sources. - -# Test Parameters -```js -var adUnits = [ - { - code: 'test-ad', - sizes: [[300, 250]], - bids: [ - { - bidder: 'progx', - params: { - cId: '562524b21b1c1f08117fc7f9', - pId: '59ac17c192832d0011283fe3', - bidFloor: 0.0001, - ext: { - param1: 'loremipsum', - param2: 'dolorsitamet' - } - } - } - ] - } -]; -``` diff --git a/test/spec/modules/programmaticXBidAdapter_spec.js b/test/spec/modules/programmaticXBidAdapter_spec.js index 2cff5d9055b..4280e6a3884 100644 --- a/test/spec/modules/programmaticXBidAdapter_spec.js +++ b/test/spec/modules/programmaticXBidAdapter_spec.js @@ -1,518 +1,708 @@ -import { expect } from 'chai'; -import { spec } from '../../../modules/programmaticXBidAdapter.js'; -import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; -import { getUniqueIdentifierStr } from '../../../src/utils.js'; - -const bidder = 'programmaticX'; - -describe('ProgrammaticXBidAdapter', function () { - const userIdAsEids = [{ - source: 'test.org', - uids: [{ - id: '01**********', - atype: 1, - ext: { - third: '01***********' - } - }] - }]; - const bids = [ - { - bidId: getUniqueIdentifierStr(), - bidder: bidder, - mediaTypes: { - [BANNER]: { - sizes: [[300, 250]] - } - }, - params: { - placementId: 'testBanner' - }, - userIdAsEids - }, - { - bidId: getUniqueIdentifierStr(), - bidder: bidder, - mediaTypes: { - [VIDEO]: { - playerSize: [[300, 300]], - minduration: 5, - maxduration: 60 - } - }, - params: { - placementId: 'testVideo' - }, - userIdAsEids - }, - { - bidId: getUniqueIdentifierStr(), - bidder: bidder, - mediaTypes: { - [NATIVE]: { - native: { - title: { - required: true - }, - body: { - required: true - }, - icon: { - required: true, - size: [64, 64] - } - } - } - }, - params: { - placementId: 'testNative' - }, - userIdAsEids +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + storage +} from 'modules/programmaticXBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes.js' +import {config} from '../../../src/config.js'; +import { + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from '../../../libraries/vidazooUtils/bidderUtils.js'; + +export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'netId', 'tdid', 'pubProvidedId', 'intentIqId', 'liveIntentId']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' } - ]; - - const invalidBid = { - bidId: getUniqueIdentifierStr(), - bidder: bidder, - mediaTypes: { - [BANNER]: { - sizes: [[300, 250]] - } - }, - params: { + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + } + } +}; +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' } } +} - const bidderRequest = { - uspConsent: '1---', - gdprConsent: { - consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', - vendorData: {} +const ORTB2_DEVICE = { + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] }, - refererInfo: { - referer: 'https://test.com', - page: 'https://test.com' - }, - ortb2: { - device: { - w: 1512, - h: 982, - language: 'en-UK' + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + ext: {fiftyonedegrees_deviceId: '17595-133085-133468-18092'}, +}; + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'site': { + 'content': { + 'language': 'en' } }, - timeout: 500 - }; + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7], + 'coppa': 0 + }, + 'device': ORTB2_DEVICE, + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['programmaticx.ai'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} - describe('isBidRequestValid', function () { - it('Should return true if there are bidId, params and key parameters present', function () { - expect(spec.isBidRequestValid(bids[0])).to.be.true; +describe('programmaticXBidAdapter', function () { + before(() => config.resetConfig()); + after(() => config.resetConfig()); + + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); }); - it('Should return false if at least one of parameters is not present', function () { - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); }); }); - describe('buildRequests', function () { - let serverRequest = spec.buildRequests(bids, bidderRequest); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(serverRequest).to.exist; - expect(serverRequest.method).to.exist; - expect(serverRequest.url).to.exist; - expect(serverRequest.data).to.exist; - }); - - it('Returns POST method', function () { - expect(serverRequest.method).to.equal('POST'); - }); - - it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://us-east.progrtb.com/pbjs'); - }); - - it('Returns general data valid', function () { - const data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', - 'deviceHeight', - 'device', - 'language', - 'secure', - 'host', - 'page', - 'placements', - 'coppa', - 'ccpa', - 'gdpr', - 'tmax', - 'bcat', - 'badv', - 'bapp', - 'battr' - ); - expect(data.deviceWidth).to.be.a('number'); - expect(data.deviceHeight).to.be.a('number'); - expect(data.language).to.be.a('string'); - expect(data.secure).to.be.within(0, 1); - expect(data.host).to.be.a('string'); - expect(data.page).to.be.a('string'); - expect(data.coppa).to.be.a('number'); - expect(data.gdpr).to.be.a('object'); - expect(data.ccpa).to.be.a('string'); - expect(data.tmax).to.be.a('number'); - expect(data.placements).to.have.lengthOf(3); - }); - - it('Returns valid placements', function () { - const { placements } = serverRequest.data; - for (let i = 0, len = placements.length; i < len; i++) { - const placement = placements[i]; - expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); - expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); - expect(placement.bidId).to.be.a('string'); - expect(placement.schain).to.be.an('object'); - expect(placement.bidfloor).to.exist.and.to.equal(0); - expect(placement.type).to.exist.and.to.equal('publisher'); - expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); - - if (placement.adFormat === BANNER) { - expect(placement.sizes).to.be.an('array'); + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' } - switch (placement.adFormat) { - case BANNER: - expect(placement.sizes).to.be.an('array'); - break; - case VIDEO: - expect(placement.playerSize).to.be.an('array'); - expect(placement.minduration).to.be.an('number'); - expect(placement.maxduration).to.be.an('number'); - break; - case NATIVE: - expect(placement.native).to.be.an('object'); - break; + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' } - } + }); + expect(isValid).to.be.true; }); + }); - it('Returns valid endpoints', function () { - const bids = [ - { - bidId: getUniqueIdentifierStr(), - bidder: bidder, + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticX: { + storageAllowed: true + } + }; + sandbox = sinon.createSandbox(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + device: ORTB2_DEVICE, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), mediaTypes: { - [BANNER]: { - sizes: [[300, 250]] + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 } }, - params: { - endpointId: 'testBanner', - }, - userIdAsEids - } - ]; - - const serverRequest = spec.buildRequests(bids, bidderRequest); - - const { placements } = serverRequest.data; - for (let i = 0, len = placements.length; i < len; i++) { - const placement = placements[i]; - expect(placement.endpointId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); - expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); - expect(placement.bidId).to.be.a('string'); - expect(placement.schain).to.be.an('object'); - expect(placement.bidfloor).to.exist.and.to.equal(0); - expect(placement.type).to.exist.and.to.equal('network'); - expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); - - if (placement.adFormat === BANNER) { - expect(placement.sizes).to.be.an('array'); + gpid: '', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } - switch (placement.adFormat) { - case BANNER: - expect(placement.sizes).to.be.an('array'); - break; - case VIDEO: - expect(placement.playerSize).to.be.an('array'); - expect(placement.minduration).to.be.an('number'); - expect(placement.maxduration).to.be.an('number'); - break; - case NATIVE: - expect(placement.native).to.be.an('object'); - break; + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + device: ORTB2_DEVICE, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } - } + }); }); - it('Returns data with gdprConsent and without uspConsent', function () { - delete bidderRequest.uspConsent; - serverRequest = spec.buildRequests(bids, bidderRequest); - const data = serverRequest.data; - expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('object'); - expect(data.gdpr).to.have.property('consentString'); - expect(data.gdpr).to.not.have.property('vendorData'); - expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); - expect(data.ccpa).to.not.exist; - delete bidderRequest.gdprConsent; - }); - - it('Returns data with uspConsent and without gdprConsent', function () { - bidderRequest.uspConsent = '1---'; - delete bidderRequest.gdprConsent; - serverRequest = spec.buildRequests(bids, bidderRequest); - const data = serverRequest.data; - expect(data.ccpa).to.exist; - expect(data.ccpa).to.be.a('string'); - expect(data.ccpa).to.equal(bidderRequest.uspConsent); - expect(data.gdpr).to.not.exist; + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); }); }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); - describe('gpp consent', function () { - it('bidderRequest.gppConsent', () => { - bidderRequest.gppConsent = { - gppString: 'abc123', - applicableSections: [8] - }; + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }]); + }); - const serverRequest = spec.buildRequests(bids, bidderRequest); - const data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.property('gpp'); - expect(data).to.have.property('gpp_sid'); + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }]); + }); - delete bidderRequest.gppConsent; - }) + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); - it('bidderRequest.ortb2.regs.gpp', () => { - bidderRequest.ortb2 = bidderRequest.ortb2 || {}; - bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; - bidderRequest.ortb2.regs.gpp = 'abc123'; - bidderRequest.ortb2.regs.gpp_sid = [8]; + expect(result).to.deep.equal([{ + 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', + 'type': 'image' + }]); + }); - const serverRequest = spec.buildRequests(bids, bidderRequest); - const data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.property('gpp'); - expect(data).to.have.property('gpp_sid'); + it('should have valid user sync with coppa 1 on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); - bidderRequest.ortb2; - }) + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&coppa=1&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); }); - describe('interpretResponse', function () { - it('Should interpret banner response', function () { - const banner = { - body: [{ - mediaType: 'banner', - width: 300, - height: 250, - cpm: 0.4, - ad: 'Test', - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['google.com'], - advertiserId: 1234 - } - }] - }; - const bannerResponses = spec.interpretResponse(banner); - expect(bannerResponses).to.be.an('array').that.is.not.empty; - const dataItem = bannerResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); - expect(dataItem.requestId).to.equal(banner.body[0].requestId); - expect(dataItem.cpm).to.equal(banner.body[0].cpm); - expect(dataItem.width).to.equal(banner.body[0].width); - expect(dataItem.height).to.equal(banner.body[0].height); - expect(dataItem.ad).to.equal(banner.body[0].ad); - expect(dataItem.ttl).to.equal(banner.body[0].ttl); - expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(banner.body[0].currency); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - }); - it('Should interpret video response', function () { - const video = { - body: [{ - vastUrl: 'test.com', - mediaType: 'video', - cpm: 0.5, - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['google.com'], - advertiserId: 1234 - } - }] - }; - const videoResponses = spec.interpretResponse(video); - expect(videoResponses).to.be.an('array').that.is.not.empty; - - const dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.5); - expect(dataItem.vastUrl).to.equal('test.com'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal('USD'); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - }); - it('Should interpret native response', function () { - const native = { - body: [{ - mediaType: 'native', - native: { - clickUrl: 'test.com', - title: 'Test', - image: 'test.com', - impressionTrackers: ['test.com'], - }, - ttl: 120, - cpm: 0.4, - requestId: '23fhj33i987f', - creativeId: '2', - netRevenue: true, - currency: 'USD', - meta: { - advertiserDomains: ['google.com'], - advertiserId: 1234 - } - }] - }; - const nativeResponses = spec.interpretResponse(native); - expect(nativeResponses).to.be.an('array').that.is.not.empty; - - const dataItem = nativeResponses[0]; - expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); - expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.native.clickUrl).to.equal('test.com'); - expect(dataItem.native.title).to.equal('Test'); - expect(dataItem.native.image).to.equal('test.com'); - expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; - expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal('USD'); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - }); - it('Should return an empty array if invalid banner response is passed', function () { - const invBanner = { - body: [{ - width: 300, - cpm: 0.4, - ad: 'Test', - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }; + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); - const serverResponses = spec.interpretResponse(invBanner); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid video response is passed', function () { - const invVideo = { - body: [{ - mediaType: 'video', - cpm: 0.5, - requestId: '23fhj33i987f', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['programmaticx.ai'], + agencyName: 'Agency Name', }; - const serverResponses = spec.interpretResponse(invVideo); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid native response is passed', function () { - const invNative = { - body: [{ - mediaType: 'native', - clickUrl: 'test.com', - title: 'Test', - impressionTrackers: ['test.com'], - ttl: 120, - requestId: '23fhj33i987f', - creativeId: '2', - netRevenue: true, - currency: 'USD', - }] + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['programmaticx.ai'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['programmaticx.ai'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId }; - const serverResponses = spec.interpretResponse(invNative); - expect(serverResponses).to.be.an('array').that.is.empty; - }); - it('Should return an empty array if invalid response is passed', function () { - const invalid = { - body: [{ - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticX: { + storageAllowed: true + } }; - const serverResponses = spec.interpretResponse(invalid); - expect(serverResponses).to.be.an('array').that.is.empty; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(storage, key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(storage, key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(storage, key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) }); }); - describe('getUserSyncs', function() { - it('Should return array of objects with proper sync config , include GDPR', function() { - const syncData = spec.getUserSyncs({}, {}, { - consentString: 'ALL', - gdprApplies: true, - }, {}); - expect(syncData).to.be.an('array').which.is.not.empty; - expect(syncData[0]).to.be.an('object') - expect(syncData[0].type).to.be.a('string') - expect(syncData[0].type).to.equal('image') - expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://sync.progrtb.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') - }); - it('Should return array of objects with proper sync config , include CCPA', function() { - const syncData = spec.getUserSyncs({}, {}, {}, { - consentString: '1---' - }); - expect(syncData).to.be.an('array').which.is.not.empty; - expect(syncData[0]).to.be.an('object') - expect(syncData[0].type).to.be.a('string') - expect(syncData[0].type).to.equal('image') - expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://sync.progrtb.com/image?pbjs=1&ccpa_consent=1---&coppa=0') - }); - it('Should return array of objects with proper sync config , include GPP', function() { - const syncData = spec.getUserSyncs({}, {}, {}, {}, { - gppString: 'abc123', - applicableSections: [8] + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + programmaticX: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now }); - expect(syncData).to.be.an('array').which.is.not.empty; - expect(syncData[0]).to.be.an('object') - expect(syncData[0].type).to.be.a('string') - expect(syncData[0].type).to.equal('image') - expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://sync.progrtb.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + setStorageItem(storage, 'myKey', 2020); + const {value, created} = getStorageItem(storage, 'myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem(storage, 'myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); }); }); + + describe('createDomain test', function() { + it('should return correct domain', function () { + const responses = createDomain(); + expect(responses).to.be.equal('https://exchange.programmaticx.ai'); + }); + }) }); diff --git a/test/spec/modules/progxBidAdapter_spec.js b/test/spec/modules/progxBidAdapter_spec.js deleted file mode 100644 index bba762c9230..00000000000 --- a/test/spec/modules/progxBidAdapter_spec.js +++ /dev/null @@ -1,708 +0,0 @@ -import {expect} from 'chai'; -import { - spec as adapter, - createDomain, - storage -} from 'modules/progxBidAdapter'; -import * as utils from 'src/utils.js'; -import {version} from 'package.json'; -import {useFakeTimers} from 'sinon'; -import {BANNER, VIDEO} from '../../../src/mediaTypes.js' -import {config} from '../../../src/config.js'; -import { - hashCode, - extractPID, - extractCID, - extractSubDomain, - getStorageItem, - setStorageItem, - tryParseJSON, - getUniqueDealId, -} from '../../../libraries/vidazooUtils/bidderUtils.js'; - -export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'netId', 'tdid', 'pubProvidedId', 'intentIqId', 'liveIntentId']; - -const SUB_DOMAIN = 'exchange'; - -const BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': 'div-gpt-ad-12345-0', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '59db6b3b4ffaa70004f45cdc', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1, - 'ext': { - 'param1': 'loremipsum', - 'param2': 'dolorsitamet' - } - }, - 'placementCode': 'div-gpt-ad-1460505748561-0', - 'sizes': [[300, 250], [300, 600]], - 'bidderRequestId': '1fdb5ff1b6eaa7', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'mediaTypes': [BANNER], - 'ortb2Imp': { - 'ext': { - 'gpid': '0123456789', - 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' - } - } -}; - -const VIDEO_BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', - 'bidderRequestId': '12a8ae9ada9c13', - 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '635509f7ff6642d368cb9837', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1 - }, - 'sizes': [[545, 307]], - 'mediaTypes': { - 'video': { - 'playerSize': [[545, 307]], - 'context': 'instream', - 'mimes': [ - 'video/mp4', - 'application/javascript' - ], - 'protocols': [2, 3, 5, 6], - 'maxduration': 60, - 'minduration': 0, - 'startdelay': 0, - 'linearity': 1, - 'api': [2], - 'placement': 1 - } - }, - 'ortb2Imp': { - 'ext': { - 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' - } - } -} - -const ORTB2_DEVICE = { - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, - {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - w: 980, - h: 1720, - dnt: 0, - ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', - language: 'en', - devicetype: 1, - make: 'Apple', - model: 'iPhone 12 Pro Max', - os: 'iOS', - osv: '17.4', - ext: {fiftyonedegrees_deviceId: '17595-133085-133468-18092'}, -}; - -const BIDDER_REQUEST = { - 'gdprConsent': { - 'consentString': 'consent_string', - 'gdprApplies': true - }, - 'gppString': 'gpp_string', - 'gppSid': [7], - 'uspConsent': 'consent_string', - 'refererInfo': { - 'page': 'https://www.greatsite.com', - 'ref': 'https://www.somereferrer.com' - }, - 'ortb2': { - 'site': { - 'content': { - 'language': 'en' - } - }, - 'regs': { - 'gpp': 'gpp_string', - 'gpp_sid': [7], - 'coppa': 0 - }, - 'device': ORTB2_DEVICE, - } -}; - -const SERVER_RESPONSE = { - body: { - cid: 'testcid123', - results: [{ - 'ad': '', - 'price': 0.8, - 'creativeId': '12610997325162499419', - 'exp': 30, - 'width': 300, - 'height': 250, - 'advertiserDomains': ['securepubads.g.doubleclick.net'], - 'cookies': [{ - 'src': 'https://sync.com', - 'type': 'iframe' - }, { - 'src': 'https://sync.com', - 'type': 'img' - }] - }] - } -}; - -const VIDEO_SERVER_RESPONSE = { - body: { - 'cid': '635509f7ff6642d368cb9837', - 'results': [{ - 'ad': '', - 'advertiserDomains': ['programmaticx.ai'], - 'exp': 60, - 'width': 545, - 'height': 307, - 'mediaType': 'video', - 'creativeId': '12610997325162499419', - 'price': 2, - 'cookies': [] - }] - } -}; - -const REQUEST = { - data: { - width: 300, - height: 250, - bidId: '2d52001cabd527' - } -}; - -function getTopWindowQueryParams() { - try { - const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} - -describe('progxBidAdapter', function () { - before(() => config.resetConfig()); - after(() => config.resetConfig()); - - describe('validtae spec', function () { - it('exists and is a function', function () { - expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); - }); - - it('exists and is a function', function () { - expect(adapter.buildRequests).to.exist.and.to.be.a('function'); - }); - - it('exists and is a function', function () { - expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); - }); - - it('exists and is a function', function () { - expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); - }); - - it('exists and is a string', function () { - expect(adapter.code).to.exist.and.to.be.a('string'); - }); - - it('exists and contains media types', function () { - expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); - expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); - }); - }); - - describe('validate bid requests', function () { - it('should require cId', function () { - const isValid = adapter.isBidRequestValid({ - params: { - pId: 'pid' - } - }); - expect(isValid).to.be.false; - }); - - it('should require pId', function () { - const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid' - } - }); - expect(isValid).to.be.false; - }); - - it('should validate correctly', function () { - const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid', - pId: 'pid' - } - }); - expect(isValid).to.be.true; - }); - }); - - describe('build requests', function () { - let sandbox; - before(function () { - $$PREBID_GLOBAL$$.bidderSettings = { - progx: { - storageAllowed: true - } - }; - sandbox = sinon.createSandbox(); - sandbox.stub(Date, 'now').returns(1000); - }); - - it('should build video request', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000 - }); - const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, - data: { - adUnitCode: '63550ad1ff6642d368cba59dh5884270560', - bidFloor: 0.1, - bidId: '2d52001cabd527', - bidderVersion: adapter.version, - bidderRequestId: '12a8ae9ada9c13', - cb: 1000, - gdpr: 1, - gdprConsent: 'consent_string', - usPrivacy: 'consent_string', - gppString: 'gpp_string', - gppSid: [7], - prebidVersion: version, - transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - publisherId: '59ac17c192832d0011283fe3', - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - res: `${window.top.screen.width}x${window.top.screen.height}`, - schain: VIDEO_BID.schain, - sizes: ['545x307'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, - {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - uqs: getTopWindowQueryParams(), - mediaTypes: { - video: { - api: [2], - context: 'instream', - linearity: 1, - maxduration: 60, - mimes: [ - 'video/mp4', - 'application/javascript' - ], - minduration: 0, - placement: 1, - playerSize: [[545, 307]], - protocols: [2, 3, 5, 6], - startdelay: 0 - } - }, - gpid: '', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - userData: [], - coppa: 0 - } - }); - }); - - it('should build banner request for each size', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000 - }); - const requests = adapter.buildRequests([BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, - data: { - gdprConsent: 'consent_string', - gdpr: 1, - gppString: 'gpp_string', - gppSid: [7], - usPrivacy: 'consent_string', - transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - bidderRequestId: '1fdb5ff1b6eaa7', - sizes: ['300x250', '300x600'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, - {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - cb: 1000, - bidFloor: 0.1, - bidId: '2d52001cabd527', - adUnitCode: 'div-gpt-ad-12345-0', - publisherId: '59ac17c192832d0011283fe3', - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - bidderVersion: adapter.version, - prebidVersion: version, - schain: BID.schain, - res: `${window.top.screen.width}x${window.top.screen.height}`, - mediaTypes: [BANNER], - gpid: '0123456789', - uqs: getTopWindowQueryParams(), - 'ext.param1': 'loremipsum', - 'ext.param2': 'dolorsitamet', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - userData: [], - coppa: 0 - } - }); - }); - - after(function () { - $$PREBID_GLOBAL$$.bidderSettings = {}; - sandbox.restore(); - }); - }); - describe('getUserSyncs', function () { - it('should have valid user sync with iframeEnabled', function () { - const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); - - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' - }]); - }); - - it('should have valid user sync with cid on response', function () { - const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' - }]); - }); - - it('should have valid user sync with pixelEnabled', function () { - const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); - - expect(result).to.deep.equal([{ - 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - 'type': 'image' - }]); - }); - - it('should have valid user sync with coppa 1 on response', function () { - config.setConfig({ - coppa: 1 - }); - const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://sync.programmaticx.ai/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' - }]); - }); - - it('should generate url with consent data', function () { - const gdprConsent = { - gdprApplies: true, - consentString: 'consent_string' - }; - const uspConsent = 'usp_string'; - const gppConsent = { - gppString: 'gpp_string', - applicableSections: [7] - } - - const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); - - expect(result).to.deep.equal([{ - 'url': 'https://sync.programmaticx.ai/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&coppa=1&gpp=gpp_string&gpp_sid=7', - 'type': 'image' - }]); - }); - }); - - describe('interpret response', function () { - it('should return empty array when there is no response', function () { - const responses = adapter.interpretResponse(null); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no ad', function () { - const responses = adapter.interpretResponse({price: 1, ad: ''}); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no price', function () { - const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); - expect(responses).to.be.empty; - }); - - it('should return an array of interpreted banner responses', function () { - const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); - expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 0.8, - width: 300, - height: 250, - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 30, - ad: '', - meta: { - advertiserDomains: ['securepubads.g.doubleclick.net'] - } - }); - }); - - it('should get meta from response metaData', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - serverResponse.body.results[0].metaData = { - advertiserDomains: ['programmaticx.ai'], - agencyName: 'Agency Name', - }; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses[0].meta).to.deep.equal({ - advertiserDomains: ['programmaticx.ai'], - agencyName: 'Agency Name' - }); - }); - - it('should return an array of interpreted video responses', function () { - const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); - expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 2, - width: 545, - height: 307, - mediaType: 'video', - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 60, - vastXml: '', - meta: { - advertiserDomains: ['programmaticx.ai'] - } - }); - }); - - it('should take default TTL', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - delete serverResponse.body.results[0].exp; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses).to.have.length(1); - expect(responses[0].ttl).to.equal(300); - }); - }); - - describe('user id system', function () { - TEST_ID_SYSTEMS.forEach((idSystemProvider) => { - const id = Date.now().toString(); - const bid = utils.deepClone(BID); - - const userId = (function () { - switch (idSystemProvider) { - case 'lipb': - return {lipbid: id}; - case 'id5id': - return {uid: id}; - default: - return id; - } - })(); - - bid.userId = { - [idSystemProvider]: userId - }; - - it(`should include 'uid.${idSystemProvider}' in request params`, function () { - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); - }); - }); - }); - - describe('alternate param names extractors', function () { - it('should return undefined when param not supported', function () { - const cid = extractCID({'c_id': '1'}); - const pid = extractPID({'p_id': '1'}); - const subDomain = extractSubDomain({'sub_domain': 'prebid'}); - expect(cid).to.be.undefined; - expect(pid).to.be.undefined; - expect(subDomain).to.be.undefined; - }); - - it('should return value when param supported', function () { - const cid = extractCID({'cID': '1'}); - const pid = extractPID({'Pid': '2'}); - const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); - expect(cid).to.be.equal('1'); - expect(pid).to.be.equal('2'); - expect(subDomain).to.be.equal('prebid'); - }); - }); - - describe('unique deal id', function () { - before(function () { - $$PREBID_GLOBAL$$.bidderSettings = { - progx: { - storageAllowed: true - } - }; - }); - after(function () { - $$PREBID_GLOBAL$$.bidderSettings = {}; - }); - const key = 'myKey'; - let uniqueDealId; - beforeEach(() => { - uniqueDealId = getUniqueDealId(storage, key, 0); - }) - - it('should get current unique deal id', function (done) { - // waiting some time so `now` will become past - setTimeout(() => { - const current = getUniqueDealId(storage, key); - expect(current).to.be.equal(uniqueDealId); - done(); - }, 200); - }); - - it('should get new unique deal id on expiration', function (done) { - setTimeout(() => { - const current = getUniqueDealId(storage, key, 100); - expect(current).to.not.be.equal(uniqueDealId); - done(); - }, 200) - }); - }); - - describe('storage utils', function () { - before(function () { - $$PREBID_GLOBAL$$.bidderSettings = { - progx: { - storageAllowed: true - } - }; - }); - after(function () { - $$PREBID_GLOBAL$$.bidderSettings = {}; - }); - it('should get value from storage with create param', function () { - const now = Date.now(); - const clock = useFakeTimers({ - shouldAdvanceTime: true, - now - }); - setStorageItem(storage, 'myKey', 2020); - const {value, created} = getStorageItem(storage, 'myKey'); - expect(created).to.be.equal(now); - expect(value).to.be.equal(2020); - expect(typeof value).to.be.equal('number'); - expect(typeof created).to.be.equal('number'); - clock.restore(); - }); - - it('should get external stored value', function () { - const value = 'superman' - window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem(storage, 'myExternalKey'); - expect(item).to.be.equal(value); - }); - - it('should parse JSON value', function () { - const data = JSON.stringify({event: 'send'}); - const {event} = tryParseJSON(data); - expect(event).to.be.equal('send'); - }); - - it('should get original value on parse fail', function () { - const value = 21; - const parsed = tryParseJSON(value); - expect(typeof parsed).to.be.equal('number'); - expect(parsed).to.be.equal(value); - }); - }); - - describe('createDomain test', function() { - it('should return correct domain', function () { - const responses = createDomain(); - expect(responses).to.be.equal('https://exchange.programmaticx.ai'); - }); - }) -}); From f14bc1d5a258176c4f188a6c792b61b881af4fa6 Mon Sep 17 00:00:00 2001 From: anna-y-perion Date: Mon, 28 Jul 2025 19:07:03 +0300 Subject: [PATCH 10/10] fix: support for placementId field --- libraries/vidazooUtils/bidderUtils.js | 3 +++ modules/programmaticXBidAdapter.md | 3 ++- test/spec/modules/programmaticXBidAdapter_spec.js | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js index 89d7447b155..8bcc6e09eb3 100644 --- a/libraries/vidazooUtils/bidderUtils.js +++ b/libraries/vidazooUtils/bidderUtils.js @@ -344,6 +344,9 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder if (dsa) { data.dsa = dsa; } + if (params.placementId) { + data.placementId = params.placementId; + } _each(ext, (value, key) => { data['ext.' + key] = value; diff --git a/modules/programmaticXBidAdapter.md b/modules/programmaticXBidAdapter.md index 89f87dff8b7..55792ae9e01 100644 --- a/modules/programmaticXBidAdapter.md +++ b/modules/programmaticXBidAdapter.md @@ -26,7 +26,8 @@ var adUnits = [ ext: { param1: 'loremipsum', param2: 'dolorsitamet' - } + }, + placementId: 'testBanner' } } ] diff --git a/test/spec/modules/programmaticXBidAdapter_spec.js b/test/spec/modules/programmaticXBidAdapter_spec.js index 4280e6a3884..91d1900d177 100644 --- a/test/spec/modules/programmaticXBidAdapter_spec.js +++ b/test/spec/modules/programmaticXBidAdapter_spec.js @@ -35,7 +35,8 @@ const BID = { 'ext': { 'param1': 'loremipsum', 'param2': 'dolorsitamet' - } + }, + 'placementId': 'testBanner' }, 'placementCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250], [300, 600]], @@ -67,7 +68,8 @@ const VIDEO_BID = { 'subDomain': SUB_DOMAIN, 'cId': '635509f7ff6642d368cb9837', 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1 + 'bidFloor': 0.1, + 'placementId': 'testBanner' }, 'sizes': [[545, 307]], 'mediaTypes': { @@ -356,6 +358,7 @@ describe('programmaticXBidAdapter', function () { contentData: [], isStorageAllowed: true, pagecat: [], + placementId: "testBanner", userData: [], coppa: 0 } @@ -424,6 +427,7 @@ describe('programmaticXBidAdapter', function () { contentData: [], isStorageAllowed: true, pagecat: [], + placementId: "testBanner", userData: [], coppa: 0 }