diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js
index 0bffb9219c..bf4b52f894 100644
--- a/modules/mediafuseBidAdapter.js
+++ b/modules/mediafuseBidAdapter.js
@@ -1,501 +1,566 @@
+import { ortbConverter } from '../libraries/ortbConverter/converter.js';
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
+import { Renderer } from '../src/Renderer.js';
+import { getStorageManager } from '../src/storageManager.js';
+import { hasPurpose1Consent } from '../src/utils/gdpr.js';
+import { bidderSettings } from '../src/bidderSettings.js';
import {
- createTrackPixelHtml,
deepAccess,
- deepClone,
- getBidRequest,
+ deepSetValue,
getParameterByName,
isArray,
isArrayOfNums,
- isEmpty,
isFn,
isNumber,
isPlainObject,
isStr,
+ isEmpty,
logError,
logInfo,
logMessage,
- logWarn
+ logWarn,
+ createTrackPixelHtml
} from '../src/utils.js';
-import {Renderer} from '../src/Renderer.js';
-import {config} from '../src/config.js';
-import {registerBidder} from '../src/adapters/bidderFactory.js';
-import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
-import {INSTREAM, OUTSTREAM} from '../src/video.js';
-import {getStorageManager} from '../src/storageManager.js';
-import {bidderSettings} from '../src/bidderSettings.js';
-import {hasPurpose1Consent} from '../src/utils/gdpr.js';
-import {convertOrtbRequestToProprietaryNative} from '../src/native.js';
-import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js';
+import { config } from '../src/config.js';
+import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js';
import {
getANKewyordParamFromMaps,
getANKeywordParam
} from '../libraries/appnexusUtils/anKeywords.js';
-import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js';
-import {chunk} from '../libraries/chunk/chunk.js';
-
-/**
- * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
- * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
- */
+import { convertCamelToUnderscore, fill } from '../libraries/appnexusUtils/anUtils.js';
+import { chunk } from '../libraries/chunk/chunk.js';
const BIDDER_CODE = 'mediafuse';
-const URL = 'https://ib.adnxs.com/ut/v3/prebid';
-const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid';
-const VIDEO_TARGETING = ['id', 'minduration', 'maxduration',
- 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset'];
-const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api'];
-const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language'];
-const APP_DEVICE_PARAMS = ['device_id']; // appid is collected separately
+const GVLID = 32;
+const ENDPOINT_URL_NORMAL = 'https://ib.adnxs.com/openrtb2/prebidjs';
+const ENDPOINT_URL_SIMPLE = 'https://ib.adnxs-simple.com/openrtb2/prebidjs';
+const SOURCE = 'pbjs';
+const MAX_IMPS_PER_REQUEST = 15;
+
const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout'];
-const VIDEO_MAPPING = {
- playback_method: {
- 'unknown': 0,
- 'auto_play_sound_on': 1,
- 'auto_play_sound_off': 2,
- 'click_to_play': 3,
- 'mouse_over': 4,
- 'auto_play_sound_unknown': 5
- },
- context: {
- 'unknown': 0,
- 'pre_roll': 1,
- 'mid_roll': 2,
- 'post_roll': 3,
- 'outstream': 4,
- 'in-banner': 5
- }
+const DEBUG_QUERY_PARAM_MAP = {
+ 'apn_debug_enabled': 'enabled',
+ 'apn_debug_dongle': 'dongle',
+ 'apn_debug_member_id': 'member_id',
+ 'apn_debug_timeout': 'debug_timeout'
};
-const NATIVE_MAPPING = {
- body: 'description',
- body2: 'desc2',
- cta: 'ctatext',
- image: {
- serverName: 'main_image',
- requiredParams: { required: true }
- },
- icon: {
- serverName: 'icon',
- requiredParams: { required: true }
- },
- sponsoredBy: 'sponsored_by',
- privacyLink: 'privacy_link',
- salePrice: 'saleprice',
- displayUrl: 'displayurl'
+
+const RESPONSE_MEDIA_TYPE_MAP = {
+ 0: BANNER,
+ 1: VIDEO,
+ 3: NATIVE
};
-const SOURCE = 'pbjs';
-const MAX_IMPS_PER_REQUEST = 15;
-const SCRIPT_TAG_START = '' }
+ }
+ }
+ }]
+ }]
+ }
+ };
+
+ const bids = spec.interpretResponse(serverResponse, req);
+ const trackers = bids[0].native.javascriptTrackers;
+ expect(trackers).to.be.an('array');
+ expect(trackers[0]).to.include('data-src=');
});
- it('should attach valid user params to the tag', function () {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: {
- placementId: '10433394',
- user: {
- externalUid: '123',
- segments: [123, { id: 987, value: 876 }],
- foobar: 'invalid'
- }
- }
+ it('should handle malformed native adm gracefully', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { native: { title: { required: true } } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const logErrorStub = sandbox.stub(utils, 'logError');
+
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: 'NOT_VALID_JSON',
+ ext: { appnexus: { bid_ad_type: 3 } }
+ }]
+ }]
}
- );
+ };
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
+ // Should not throw
+ expect(() => spec.interpretResponse(serverResponse, req)).to.not.throw();
+ expect(logErrorStub.calledOnce).to.be.true;
+ });
+ });
- expect(payload.user).to.exist;
- expect(payload.user).to.deep.equal({
- external_uid: '123',
- segments: [{id: 123}, {id: 987, value: 876}]
- });
+ // -------------------------------------------------------------------------
+ // getUserSyncs — gdprApplies not a boolean
+ // -------------------------------------------------------------------------
+ describe('getUserSyncs - gdprApplies undefined', function () {
+ it('should use only gdpr_consent param when gdprApplies is not a boolean', function () {
+ const syncOptions = { pixelEnabled: true };
+ const serverResponses = [{
+ body: { ext: { appnexus: { userSync: { url: 'https://sync.example.com/px' } } } }
+ }];
+ const gdprConsent = { consentString: 'abc123' }; // gdprApplies is undefined
+
+ const syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent);
+ expect(syncs).to.have.lengthOf(1);
+ expect(syncs[0].url).to.include('gdpr_consent=abc123');
+ expect(syncs[0].url).to.not.include('gdpr=');
});
+ });
- it('should attach reserve param when either bid param or getFloor function exists', function () {
- const getFloorResponse = { currency: 'USD', floor: 3 };
- let request; let payload = null;
- const bidRequest = deepClone(bidRequests[0]);
+ // -------------------------------------------------------------------------
+ // lifecycle — onBidWon
+ // -------------------------------------------------------------------------
+ describe('onBidWon', function () {
+ it('should call reloadViewabilityScript for native bids without throwing', function () {
+ const bid = {
+ adId: 'ad-1',
+ adUnitCode: 'unit-1',
+ native: { javascriptTrackers: ['//cdn.adnxs.com/v/trk.js?dom_id=%native_dom_id%'] }
+ };
+ expect(() => spec.onBidWon(bid)).to.not.throw();
+ });
- // 1 -> reserve not defined, getFloor not defined > empty
- request = spec.buildRequests([bidRequest]);
- payload = JSON.parse(request.data);
+ it('should not throw for non-native bids', function () {
+ const bid = { adId: 'ad-2', adUnitCode: 'unit-2', ad: '
Banner
' };
+ expect(() => spec.onBidWon(bid)).to.not.throw();
+ });
+ });
- expect(payload.tags[0].reserve).to.not.exist;
+ // -------------------------------------------------------------------------
+ // lifecycle — onBidderError
+ // -------------------------------------------------------------------------
+ describe('onBidderError', function () {
+ it('should call logMessage with error details', function () {
+ const logStub = sandbox.stub(utils, 'logMessage');
+ const error = new Error('timeout');
+ spec.onBidderError({ error, bidderRequest: { auctionId: 'x' } });
+ expect(logStub.calledOnce).to.be.true;
+ expect(logStub.firstCall.args[0]).to.include('Mediafuse Bidder Error');
+ });
+ });
- // 2 -> reserve is defined, getFloor not defined > reserve is used
- bidRequest.params = {
- 'placementId': '10433394',
- 'reserve': 0.5
+ // -------------------------------------------------------------------------
+ // interpretResponse — dchain from buyer_member_id
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - dchain', function () {
+ it('should set meta.dchain when buyer_member_id is present', function () {
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ ext: { appnexus: { bid_ad_type: 0, buyer_member_id: 77, advertiser_id: 99 } }
+ }]
+ }]
+ }
};
- request = spec.buildRequests([bidRequest]);
- payload = JSON.parse(request.data);
- expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5);
-
- // 3 -> reserve is defined, getFloor is defined > getFloor is used
- bidRequest.getFloor = () => getFloorResponse;
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].meta.dchain).to.deep.equal({
+ ver: '1.0',
+ complete: 0,
+ nodes: [{ bsid: '77' }]
+ });
+ expect(bids[0].meta.advertiserId).to.equal(99);
+ });
+ });
- request = spec.buildRequests([bidRequest]);
- payload = JSON.parse(request.data);
+ // -------------------------------------------------------------------------
+ // buildRequests — optional params map (allowSmallerSizes, usePaymentRule, etc.)
+ // -------------------------------------------------------------------------
+ describe('buildRequests - optional params', function () {
+ it('should map allowSmallerSizes, usePaymentRule, trafficSourceCode', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.allowSmallerSizes = true;
+ bid.params.usePaymentRule = true;
+ bid.params.trafficSourceCode = 'my-source';
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const extAN = req.data.imp[0].ext.appnexus;
+ expect(extAN.allow_smaller_sizes).to.be.true;
+ expect(extAN.use_pmt_rule).to.be.true;
+ expect(extAN.traffic_source_code).to.equal('my-source');
+ });
- expect(payload.tags[0].reserve).to.exist.and.to.equal(3);
+ it('should map externalImpId to imp.id', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.externalImpId = 'ext-imp-123';
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].id).to.equal('ext-imp-123');
});
+ });
+});
- it('should duplicate adpod placements into batches and set correct maxduration', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 300,
- durationRangeSec: [15, 30],
- }
- }
- }
- );
+describe('mediafuseBidAdapter', function () {
+ let sandbox;
- const request = spec.buildRequests([bidRequest]);
- const payload1 = JSON.parse(request[0].data);
- const payload2 = JSON.parse(request[1].data);
+ beforeEach(function () {
+ sandbox = sinon.createSandbox();
+ });
- // 300 / 15 = 20 total
- expect(payload1.tags.length).to.equal(15);
- expect(payload2.tags.length).to.equal(5);
+ afterEach(function () {
+ sandbox.restore();
+ });
- expect(payload1.tags[0]).to.deep.equal(payload1.tags[1]);
- expect(payload1.tags[0].video.maxduration).to.equal(30);
+ // -------------------------------------------------------------------------
+ // isBidRequestValid
+ // -------------------------------------------------------------------------
+ describe('isBidRequestValid', function () {
+ it('should return true for placement_id (snake_case)', function () {
+ expect(spec.isBidRequestValid({ params: { placement_id: 12345 } })).to.be.true;
+ });
- expect(payload2.tags[0]).to.deep.equal(payload1.tags[1]);
- expect(payload2.tags[0].video.maxduration).to.equal(30);
+ it('should return true for member + invCode', function () {
+ expect(spec.isBidRequestValid({ params: { member: '123', invCode: 'inv' } })).to.be.true;
});
- it('should round down adpod placements when numbers are uneven', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 123,
- durationRangeSec: [45],
- }
- }
- }
- );
+ it('should return true for member + inv_code', function () {
+ expect(spec.isBidRequestValid({ params: { member: '123', inv_code: 'inv' } })).to.be.true;
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.tags.length).to.equal(2);
+ it('should return false when no params', function () {
+ expect(spec.isBidRequestValid({})).to.be.false;
});
- it('should duplicate adpod placements when requireExactDuration is set', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 300,
- durationRangeSec: [15, 30],
- requireExactDuration: true,
- }
- }
- }
- );
-
- // 20 total placements with 15 max impressions = 2 requests
- const request = spec.buildRequests([bidRequest]);
- expect(request.length).to.equal(2);
-
- // 20 spread over 2 requests = 15 in first request, 5 in second
- const payload1 = JSON.parse(request[0].data);
- const payload2 = JSON.parse(request[1].data);
- expect(payload1.tags.length).to.equal(15);
- expect(payload2.tags.length).to.equal(5);
-
- // 10 placements should have max/min at 15
- // 10 placemenst should have max/min at 30
- const payload1tagsWith15 = payload1.tags.filter(tag => tag.video.maxduration === 15);
- const payload1tagsWith30 = payload1.tags.filter(tag => tag.video.maxduration === 30);
- expect(payload1tagsWith15.length).to.equal(10);
- expect(payload1tagsWith30.length).to.equal(5);
-
- // 5 placemenst with min/max at 30 were in the first request
- // so 5 remaining should be in the second
- const payload2tagsWith30 = payload2.tags.filter(tag => tag.video.maxduration === 30);
- expect(payload2tagsWith30.length).to.equal(5);
- });
-
- it('should set durations for placements when requireExactDuration is set and numbers are uneven', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 105,
- durationRangeSec: [15, 30, 60],
- requireExactDuration: true,
- }
- }
- }
- );
+ it('should return false for member without invCode or inv_code', function () {
+ expect(spec.isBidRequestValid({ params: { member: '123' } })).to.be.false;
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.tags.length).to.equal(7);
+ // -------------------------------------------------------------------------
+ // getBidFloor
+ // -------------------------------------------------------------------------
+ describe('buildRequests - getBidFloor', function () {
+ it('should use getFloor function result when available and currency matches', function () {
+ const bid = deepClone(BASE_BID);
+ bid.getFloor = () => ({ currency: 'USD', floor: 1.5 });
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].bidfloor).to.equal(1.5);
+ });
- const tagsWith15 = payload.tags.filter(tag => tag.video.maxduration === 15);
- const tagsWith30 = payload.tags.filter(tag => tag.video.maxduration === 30);
- const tagsWith60 = payload.tags.filter(tag => tag.video.maxduration === 60);
- expect(tagsWith15.length).to.equal(3);
- expect(tagsWith30.length).to.equal(3);
- expect(tagsWith60.length).to.equal(1);
+ it('should return null when getFloor returns wrong currency', function () {
+ const bid = deepClone(BASE_BID);
+ bid.getFloor = () => ({ currency: 'EUR', floor: 1.5 });
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].bidfloor).to.be.undefined;
});
- it('should break adpod request into batches', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 225,
- durationRangeSec: [5],
- }
- }
- }
- );
+ it('should use params.reserve when no getFloor function', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.reserve = 2.0;
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].bidfloor).to.equal(2.0);
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload1 = JSON.parse(request[0].data);
- const payload2 = JSON.parse(request[1].data);
- const payload3 = JSON.parse(request[2].data);
+ // -------------------------------------------------------------------------
+ // buildRequests — inv_code
+ // -------------------------------------------------------------------------
+ describe('buildRequests - inv_code', function () {
+ it('should set tagid from invCode when no placementId', function () {
+ const bid = { bidder: 'mediafuse', adUnitCode: 'au', bidId: 'b1', params: { invCode: 'my-inv-code', member: '123' } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].tagid).to.equal('my-inv-code');
+ });
- expect(payload1.tags.length).to.equal(15);
- expect(payload2.tags.length).to.equal(15);
- expect(payload3.tags.length).to.equal(15);
+ it('should set tagid from inv_code when no placementId', function () {
+ const bid = { bidder: 'mediafuse', adUnitCode: 'au', bidId: 'b1', params: { inv_code: 'my-inv-code-snake', member: '123' } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].tagid).to.equal('my-inv-code-snake');
});
+ });
- it('should contain hb_source value for adpod', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: { placementId: '14542875' }
- },
- {
- mediaTypes: {
- video: {
- context: 'adpod',
- playerSize: [640, 480],
- adPodDurationSec: 300,
- durationRangeSec: [15, 30],
- }
- }
- }
- );
- const request = spec.buildRequests([bidRequest])[0];
- const payload = JSON.parse(request.data);
- expect(payload.tags[0].hb_source).to.deep.equal(7);
- });
-
- it('should contain hb_source value for other media', function() {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- mediaType: 'banner',
- params: {
- sizes: [[300, 250], [300, 600]],
- placementId: 13144370
- }
- }
- );
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.tags[0].hb_source).to.deep.equal(1);
- });
-
- it('adds brand_category_exclusion to request when set', function() {
- const bidRequest = Object.assign({}, bidRequests[0]);
- sinon
- .stub(config, 'getConfig')
- .withArgs('adpod.brandCategoryExclusion')
- .returns(true);
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
-
- expect(payload.brand_category_uniqueness).to.equal(true);
-
- config.getConfig.restore();
- });
-
- it('adds auction level keywords to request when set', function() {
- const bidRequest = Object.assign({}, bidRequests[0]);
- sinon
- .stub(config, 'getConfig')
- .withArgs('mediafuseAuctionKeywords')
- .returns({
- gender: 'm',
- music: ['rock', 'pop'],
- test: ''
- });
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
-
- expect(payload.keywords).to.deep.equal([{
- 'key': 'gender',
- 'value': ['m']
- }, {
- 'key': 'music',
- 'value': ['rock', 'pop']
- }, {
- 'key': 'test'
- }]);
-
- config.getConfig.restore();
- });
-
- it('should attach native params to the request', function () {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- mediaType: 'native',
- nativeParams: {
- title: {required: true},
- body: {required: true},
- body2: {required: true},
- image: {required: true, sizes: [100, 100]},
- icon: {required: true},
- cta: {required: false},
- rating: {required: true},
- sponsoredBy: {required: true},
- privacyLink: {required: true},
- displayUrl: {required: true},
- address: {required: true},
- downloads: {required: true},
- likes: {required: true},
- phone: {required: true},
- price: {required: true},
- salePrice: {required: true}
- }
- }
- );
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
-
- expect(payload.tags[0].native.layouts[0]).to.deep.equal({
- title: {required: true},
- description: {required: true},
- desc2: {required: true},
- main_image: {required: true, sizes: [{ width: 100, height: 100 }]},
- icon: {required: true},
- ctatext: {required: false},
- rating: {required: true},
- sponsored_by: {required: true},
- privacy_link: {required: true},
- displayurl: {required: true},
- address: {required: true},
- downloads: {required: true},
- likes: {required: true},
- phone: {required: true},
- price: {required: true},
- saleprice: {required: true},
- privacy_supported: true
- });
- expect(payload.tags[0].hb_source).to.equal(1);
+ // -------------------------------------------------------------------------
+ // buildRequests — banner_frameworks
+ // -------------------------------------------------------------------------
+ describe('buildRequests - banner_frameworks', function () {
+ it('should set banner_frameworks from bid.params.banner_frameworks when no banner.api', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { banner: { sizes: [[300, 250]] } };
+ bid.params.banner_frameworks = [1, 2, 3];
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.banner_frameworks).to.deep.equal([1, 2, 3]);
});
+ });
- it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- mediaType: 'native',
- nativeParams: {
- image: { required: true }
- }
- }
- );
- bidRequest.sizes = [[150, 100], [300, 250]];
-
- let request = spec.buildRequests([bidRequest]);
- let payload = JSON.parse(request.data);
- expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]);
-
- delete bidRequest.sizes;
-
- request = spec.buildRequests([bidRequest]);
- payload = JSON.parse(request.data);
-
- expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]);
- });
-
- it('should convert keyword params to proper form and attaches to request', function () {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: {
- placementId: '10433394',
- keywords: {
- single: 'val',
- singleArr: ['val'],
- singleArrNum: [5],
- multiValMixed: ['value1', 2, 'value3'],
- singleValNum: 123,
- emptyStr: '',
- emptyArr: [''],
- badValue: {'foo': 'bar'} // should be dropped
- }
- }
- }
- );
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
-
- expect(payload.tags[0].keywords).to.deep.equal([{
- 'key': 'single',
- 'value': ['val']
- }, {
- 'key': 'singleArr',
- 'value': ['val']
- }, {
- 'key': 'singleArrNum',
- 'value': ['5']
- }, {
- 'key': 'multiValMixed',
- 'value': ['value1', '2', 'value3']
- }, {
- 'key': 'singleValNum',
- 'value': ['123']
- }, {
- 'key': 'emptyStr'
- }, {
- 'key': 'emptyArr'
- }]);
- });
-
- it('should add payment rules to the request', function () {
- const bidRequest = Object.assign({},
- bidRequests[0],
- {
- params: {
- placementId: '10433394',
- usePaymentRule: true
- }
- }
- );
+ // -------------------------------------------------------------------------
+ // buildRequests — custom_renderer_present via bid.renderer
+ // -------------------------------------------------------------------------
+ describe('buildRequests - custom renderer present', function () {
+ it('should set custom_renderer_present when bid.renderer is set for video imp', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'outstream', playerSize: [640, 480] } };
+ bid.renderer = { id: 'custom', url: 'https://renderer.example.com/r.js' };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.custom_renderer_present).to.be.true;
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // buildRequests — catch-all unknown camelCase params
+ // -------------------------------------------------------------------------
+ describe('buildRequests - catch-all unknown params', function () {
+ it('should convert unknown camelCase params to snake_case in extAN', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.unknownCamelCaseParam = 'value123';
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.unknown_camel_case_param).to.equal('value123');
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // buildRequests — bid-level keywords
+ // -------------------------------------------------------------------------
+ describe('buildRequests - bid keywords', function () {
+ it('should map bid.params.keywords to extAN.keywords string', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.keywords = { genre: ['rock', 'pop'] };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.keywords).to.be.a('string');
+ expect(req.data.imp[0].ext.appnexus.keywords).to.include('genre=rock,pop');
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
+ // -------------------------------------------------------------------------
+ // buildRequests — canonicalUrl in referer detection
+ // -------------------------------------------------------------------------
+ describe('buildRequests - canonicalUrl', function () {
+ it('should set rd_can in referrer_detection when canonicalUrl is present', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.refererInfo.canonicalUrl = 'https://canonical.example.com/page';
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.ext.appnexus.referrer_detection.rd_can).to.equal('https://canonical.example.com/page');
+ });
+ });
- expect(payload.tags[0].use_pmt_rule).to.equal(true);
+ // -------------------------------------------------------------------------
+ // buildRequests — publisherId → site.publisher.id
+ // -------------------------------------------------------------------------
+ describe('buildRequests - publisherId', function () {
+ it('should set site.publisher.id from bid.params.publisherId', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.publisherId = 67890;
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.bids = [bid];
+ const [req] = spec.buildRequests([bid], bidderRequest);
+ expect(req.data.site.publisher.id).to.equal('67890');
});
+ });
- it('should add gpid to the request', function () {
- const testGpid = '/12345/my-gpt-tag-0';
- const bidRequest = deepClone(bidRequests[0]);
- bidRequest.ortb2Imp = { ext: { data: {}, gpid: testGpid } };
+ // -------------------------------------------------------------------------
+ // buildRequests — member appended to endpoint URL
+ // -------------------------------------------------------------------------
+ describe('buildRequests - member URL param', function () {
+ it('should append member_id to endpoint URL when bid.params.member is set', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.member = '456';
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.bids = [bid];
+ const [req] = spec.buildRequests([bid], bidderRequest);
+ expect(req.url).to.include('member_id=456');
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
+ // -------------------------------------------------------------------------
+ // buildRequests — gppConsent
+ // -------------------------------------------------------------------------
+ describe('buildRequests - gppConsent', function () {
+ it('should set regs.gpp and regs.gpp_sid from gppConsent', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.gppConsent = { gppString: 'DBACMYA', applicableSections: [7] };
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.regs.gpp).to.equal('DBACMYA');
+ expect(req.data.regs.gpp_sid).to.deep.equal([7]);
+ });
+ });
- expect(payload.tags[0].gpid).to.exist.and.equal(testGpid)
+ // -------------------------------------------------------------------------
+ // buildRequests — gdprApplies=false
+ // -------------------------------------------------------------------------
+ describe('buildRequests - gdprApplies false', function () {
+ it('should set regs.ext.gdpr=0 when gdprApplies is false', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.gdprConsent = { gdprApplies: false, consentString: 'cs' };
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.regs.ext.gdpr).to.equal(0);
});
+ });
- it('should add gdpr consent information to the request', function () {
- const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
- const bidderRequest = {
- 'bidderCode': 'mediafuse',
- 'auctionId': '1d1a030790a475',
- 'bidderRequestId': '22edbae2733bf6',
- 'timeout': 3000,
- 'gdprConsent': {
- consentString: consentString,
- gdprApplies: true,
- addtlConsent: '1~7.12.35.62.66.70.89.93.108'
- }
- };
- bidderRequest.bids = bidRequests;
-
- const request = spec.buildRequests(bidRequests, bidderRequest);
- expect(request.options).to.deep.equal({withCredentials: true});
- const payload = JSON.parse(request.data);
-
- expect(payload.gdpr_consent).to.exist;
- expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString);
- expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true;
- expect(payload.gdpr_consent.addtl_consent).to.exist.and.to.deep.equal([7, 12, 35, 62, 66, 70, 89, 93, 108]);
- });
-
- it('should add us privacy string to payload', function() {
- const consentString = '1YA-';
- const bidderRequest = {
- 'bidderCode': 'mediafuse',
- 'auctionId': '1d1a030790a475',
- 'bidderRequestId': '22edbae2733bf6',
- 'timeout': 3000,
- 'uspConsent': consentString
- };
- bidderRequest.bids = bidRequests;
-
- const request = spec.buildRequests(bidRequests, bidderRequest);
- const payload = JSON.parse(request.data);
-
- expect(payload.us_privacy).to.exist;
- expect(payload.us_privacy).to.exist.and.to.equal(consentString);
- });
-
- it('supports sending hybrid mobile app parameters', function () {
- const appRequest = Object.assign({},
- bidRequests[0],
- {
- params: {
- placementId: '10433394',
- app: {
- id: 'B1O2W3M4AN.com.prebid.webview',
- geo: {
- lat: 40.0964439,
- lng: -75.3009142
- },
- device_id: {
- idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier
- aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier
- md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID
- sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID
- windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier
- }
- }
- }
- }
- );
- const request = spec.buildRequests([appRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.app).to.exist;
- expect(payload.app).to.deep.equal({
- appid: 'B1O2W3M4AN.com.prebid.webview'
- });
- expect(payload.device.device_id).to.exist;
- expect(payload.device.device_id).to.deep.equal({
- aaid: '38400000-8cf0-11bd-b23e-10b96e40000d',
- idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE',
- md5udid: '5756ae9022b2ea1e47d84fead75220c8',
- sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF',
- windowsadid: '750c6be243f1c4b5c9912b95a5742fc5'
- });
- expect(payload.device.geo).to.not.exist;
- expect(payload.device.geo).to.not.deep.equal({
- lat: 40.0964439,
- lng: -75.3009142
- });
+ // -------------------------------------------------------------------------
+ // buildRequests — user.externalUid
+ // -------------------------------------------------------------------------
+ describe('buildRequests - user externalUid', function () {
+ it('should map externalUid to user.external_uid', function () {
+ const bid = deepClone(BASE_BID);
+ bid.params.user = { externalUid: 'uid-abc-123' };
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.bids = [bid];
+ const [req] = spec.buildRequests([bid], bidderRequest);
+ expect(req.data.user.external_uid).to.equal('uid-abc-123');
});
+ });
- it('should add referer info to payload', function () {
- const bidRequest = Object.assign({}, bidRequests[0])
- const bidderRequest = {
- refererInfo: {
- topmostLocation: 'https://example.com/page.html',
- reachedTop: true,
- numIframes: 2,
- stack: [
- 'https://example.com/page.html',
- 'https://example.com/iframe1.html',
- 'https://example.com/iframe2.html'
- ]
- }
+ // -------------------------------------------------------------------------
+ // buildRequests — EID rti_partner mapping (TDID / UID2)
+ // -------------------------------------------------------------------------
+ describe('buildRequests - EID rti_partner mapping', function () {
+ it('should add rti_partner=TDID to adserver.org EID', function () {
+ const bid = deepClone(BASE_BID);
+ bid.userIdAsEids = [{ source: 'adserver.org', uids: [{ id: 'tdid-value', atype: 1 }] }];
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const eid = req.data.user && req.data.user.ext && req.data.user.ext.eids &&
+ req.data.user.ext.eids.find(e => e.source === 'adserver.org');
+ if (eid) {
+ expect(eid.rti_partner).to.equal('TDID');
}
- const request = spec.buildRequests([bidRequest], bidderRequest);
- const payload = JSON.parse(request.data);
-
- expect(payload.referrer_detection).to.exist;
- expect(payload.referrer_detection).to.deep.equal({
- rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html',
- rd_top: true,
- rd_ifs: 2,
- rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',')
- });
});
- it('should populate schain if available', function () {
- const bidRequest = Object.assign({}, bidRequests[0], {
- ortb2: {
- source: {
- ext: {
- schain: {
- ver: '1.0',
- complete: 1,
- nodes: [
- {
- 'asi': 'blob.com',
- 'sid': '001',
- 'hp': 1
- }
- ]
- }
- }
- }
- }
- });
+ it('should add rti_partner=UID2 to uidapi.com EID', function () {
+ const bid = deepClone(BASE_BID);
+ bid.userIdAsEids = [{ source: 'uidapi.com', uids: [{ id: 'uid2-value', atype: 3 }] }];
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const eid = req.data.user && req.data.user.ext && req.data.user.ext.eids &&
+ req.data.user.ext.eids.find(e => e.source === 'uidapi.com');
+ if (eid) {
+ expect(eid.rti_partner).to.equal('UID2');
+ }
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.schain).to.deep.equal({
- ver: '1.0',
- complete: 1,
- nodes: [
- {
- 'asi': 'blob.com',
- 'sid': '001',
- 'hp': 1
- }
- ]
+ // -------------------------------------------------------------------------
+ // buildRequests — apn_test config → X-Is-Test header
+ // -------------------------------------------------------------------------
+ describe('buildRequests - apn_test config header', function () {
+ it('should set X-Is-Test:1 custom header when config apn_test=true', function () {
+ sandbox.stub(config, 'getConfig').callsFake((key) => {
+ if (key === 'apn_test') return true;
+ return undefined;
});
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.options.customHeaders).to.deep.equal({ 'X-Is-Test': 1 });
});
+ });
- it('should populate coppa if set in config', function () {
- const bidRequest = Object.assign({}, bidRequests[0]);
- sinon.stub(config, 'getConfig')
- .withArgs('coppa')
- .returns(true);
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
+ // -------------------------------------------------------------------------
+ // buildRequests — adpod early return (no durationRangeSec)
+ // -------------------------------------------------------------------------
+ describe('buildRequests - adpod early return', function () {
+ it('should return single imp when adpod has no durationRangeSec', function () {
+ const bid = {
+ bidder: 'mediafuse',
+ adUnitCode: 'pod',
+ bidId: 'pod-bid',
+ mediaTypes: { video: { context: 'adpod', adPodDurationSec: 30 } },
+ params: { placementId: 111 }
+ };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp).to.have.lengthOf(1);
+ });
+ });
- expect(payload.user.coppa).to.equal(true);
+ // -------------------------------------------------------------------------
+ // buildRequests — video minduration already set (skip overwrite)
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video minduration skip overwrite', function () {
+ it('should not overwrite minduration set by params.video when mediaTypes.video.minduration also present', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], minduration: 10 } };
+ bid.params.video = { minduration: 5 };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ // params.video sets minduration=5 first; mediaTypes check sees it's already a number → skips
+ expect(req.data.imp[0].video.minduration).to.equal(5);
+ });
+ });
- config.getConfig.restore();
+ // -------------------------------------------------------------------------
+ // buildRequests — playbackmethod out of range (>4)
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video playbackmethod out of range', function () {
+ it('should not set playback_method when playbackmethod[0] > 4', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], playbackmethod: [5] } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].video.playback_method).to.be.undefined;
});
+ });
- it('should set the X-Is-Test customHeader if test flag is enabled', function () {
- const bidRequest = Object.assign({}, bidRequests[0]);
- sinon.stub(config, 'getConfig')
- .withArgs('apn_test')
- .returns(true);
+ // -------------------------------------------------------------------------
+ // buildRequests — video api val=6 filtered out
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video api val=6 filtered', function () {
+ it('should produce empty video_frameworks when api=[6] since 6 is out of 1-5 range', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], api: [6] } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.video_frameworks).to.deep.equal([]);
+ });
+ });
- const request = spec.buildRequests([bidRequest]);
- expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1});
+ // -------------------------------------------------------------------------
+ // buildRequests — video_frameworks already set; api should not override
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video_frameworks not overridden by api', function () {
+ it('should keep frameworks from params.video when mediaTypes.video.api is also present', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], api: [4] } };
+ bid.params.video = { frameworks: [1, 2] };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.video_frameworks).to.deep.equal([1, 2]);
+ });
+ });
- config.getConfig.restore();
+ // -------------------------------------------------------------------------
+ // interpretResponse — adomain string vs empty array
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - adomain handling', function () {
+ it('should wrap string adomain in an array for advertiserDomains', function () {
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adomain: 'example.com',
+ ext: { appnexus: { bid_ad_type: 0 } }
+ }]
+ }]
+ }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].meta.advertiserDomains).to.deep.equal(['example.com']);
});
- it('should always set withCredentials: true on the request.options', function () {
- const bidRequest = Object.assign({}, bidRequests[0]);
- const request = spec.buildRequests([bidRequest]);
- expect(request.options.withCredentials).to.equal(true);
+ it('should not set non-empty advertiserDomains when adomain is an empty array', function () {
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adomain: [],
+ ext: { appnexus: { bid_ad_type: 0 } }
+ }]
+ }]
+ }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ // adapter's guard skips setting advertiserDomains for empty arrays;
+ // ortbConverter may set it to [] — either way it must not be a non-empty array
+ const domains = bids[0].meta && bids[0].meta.advertiserDomains;
+ expect(!domains || domains.length === 0).to.be.true;
});
+ });
- it('should set simple domain variant if purpose 1 consent is not given', function () {
- const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
- const bidderRequest = {
- 'bidderCode': 'mediafuse',
- 'auctionId': '1d1a030790a475',
- 'bidderRequestId': '22edbae2733bf6',
- 'timeout': 3000,
- 'gdprConsent': {
- consentString: consentString,
- gdprApplies: true,
- apiVersion: 2,
- vendorData: {
- purpose: {
- consents: {
- 1: false
+ // -------------------------------------------------------------------------
+ // interpretResponse — banner impression_urls trackers
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - banner trackers', function () {
+ it('should append tracker pixel HTML to bid.ad when trackers.impression_urls is present', function () {
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: 'ad
',
+ ext: {
+ appnexus: {
+ bid_ad_type: 0,
+ trackers: [{ impression_urls: ['https://tracker.example.com/impression'] }]
+ }
}
- }
- }
+ }]
+ }]
}
};
- bidderRequest.bids = bidRequests;
-
- const request = spec.buildRequests(bidRequests, bidderRequest);
- expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid');
- });
-
- it('should populate eids when supported userIds are available', function () {
- const bidRequest = Object.assign({}, bidRequests[0], {
- userIdAsEids: [{
- source: 'adserver.org',
- uids: [{ id: 'sample-userid' }]
- }, {
- source: 'criteo.com',
- uids: [{ id: 'sample-criteo-userid' }]
- }, {
- source: 'netid.de',
- uids: [{ id: 'sample-netId-userid' }]
- }, {
- source: 'liveramp.com',
- uids: [{ id: 'sample-idl-userid' }]
- }, {
- source: 'uidapi.com',
- uids: [{ id: 'sample-uid2-value' }]
- }, {
- source: 'puburl.com',
- uids: [{ id: 'pubid1' }]
- }, {
- source: 'puburl2.com',
- uids: [{ id: 'pubid2' }, { id: 'pubid2-123' }]
- }]
- });
-
- const request = spec.buildRequests([bidRequest]);
- const payload = JSON.parse(request.data);
- expect(payload.eids).to.deep.include({
- source: 'adserver.org',
- id: 'sample-userid',
- rti_partner: 'TDID'
- });
-
- expect(payload.eids).to.deep.include({
- source: 'criteo.com',
- id: 'sample-criteo-userid',
- });
-
- expect(payload.eids).to.deep.include({
- source: 'netid.de',
- id: 'sample-netId-userid',
- });
-
- expect(payload.eids).to.deep.include({
- source: 'liveramp.com',
- id: 'sample-idl-userid'
- });
-
- expect(payload.eids).to.deep.include({
- source: 'uidapi.com',
- id: 'sample-uid2-value',
- rti_partner: 'UID2'
- });
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].ad).to.include('tracker.example.com/impression');
});
+ });
- it('should populate iab_support object at the root level if omid support is detected', function () {
- // with bid.params.frameworks
- const bidRequest_A = Object.assign({}, bidRequests[0], {
- params: {
- frameworks: [1, 2, 5, 6],
- video: {
- frameworks: [1, 2, 5, 6]
- }
- }
- });
- let request = spec.buildRequests([bidRequest_A]);
- let payload = JSON.parse(request.data);
- expect(payload.iab_support).to.be.an('object');
- expect(payload.iab_support).to.deep.equal({
- omidpn: 'Mediafuse',
- omidpv: '$prebid.version$'
- });
- expect(payload.tags[0].banner_frameworks).to.be.an('array');
- expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]);
- expect(payload.tags[0].video_frameworks).to.be.an('array');
- expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]);
- expect(payload.tags[0].video.frameworks).to.not.exist;
-
- // without bid.params.frameworks
- const bidRequest_B = Object.assign({}, bidRequests[0]);
- request = spec.buildRequests([bidRequest_B]);
- payload = JSON.parse(request.data);
- expect(payload.iab_support).to.not.exist;
- expect(payload.tags[0].banner_frameworks).to.not.exist;
- expect(payload.tags[0].video_frameworks).to.not.exist;
-
- // with video.frameworks but it is not an array
- const bidRequest_C = Object.assign({}, bidRequests[0], {
- params: {
- video: {
- frameworks: "'1', '2', '3', '6'"
- }
+ // -------------------------------------------------------------------------
+ // interpretResponse — native jsTrackers combinations
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - native jsTrackers combinations', function () {
+ function buildNativeReq() {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { native: { title: { required: true } } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ return req;
+ }
+
+ it('should combine string jsTracker with viewability.config into array [str, disarmed]', function () {
+ const req = buildNativeReq();
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: JSON.stringify({ native: { title: 'T', javascript_trackers: 'https://existing-tracker.com/t.js' } }),
+ ext: {
+ appnexus: {
+ bid_ad_type: 3,
+ viewability: { config: '' }
+ }
+ }
+ }]
+ }]
}
- });
- request = spec.buildRequests([bidRequest_C]);
- payload = JSON.parse(request.data);
- expect(payload.iab_support).to.not.exist;
- expect(payload.tags[0].banner_frameworks).to.not.exist;
- expect(payload.tags[0].video_frameworks).to.not.exist;
- });
- })
-
- describe('interpretResponse', function () {
- let bidderSettingsStorage;
-
- before(function() {
- bidderSettingsStorage = getGlobal().bidderSettings;
- });
-
- after(function() {
- getGlobal().bidderSettings = bidderSettingsStorage;
- });
-
- const response = {
- 'version': '3.0.0',
- 'tags': [
- {
- 'uuid': '3db3773286ee59',
- 'tag_id': 10433394,
- 'auction_id': '4534722592064951574',
- 'nobid': false,
- 'no_ad_url': 'https://lax1-ib.adnxs.com/no-ad',
- 'timeout_ms': 10000,
- 'ad_profile_id': 27079,
- 'ads': [
- {
- 'content_source': 'rtb',
- 'ad_type': 'banner',
- 'buyer_member_id': 958,
- 'creative_id': 29681110,
- 'media_type_id': 1,
- 'media_subtype_id': 1,
- 'cpm': 0.5,
- 'cpm_publisher_currency': 0.5,
- 'publisher_currency_code': '$',
- 'client_initiated_ad_counting': true,
- 'viewability': {
- 'config': ''
- },
- 'rtb': {
- 'banner': {
- 'content': '',
- 'width': 300,
- 'height': 250
- },
- 'trackers': [
- {
- 'impression_urls': [
- 'https://lax1-ib.adnxs.com/impression',
- 'https://www.test.com/tracker'
- ],
- 'video_events': {}
- }
- ]
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ const trackers = bids[0].native.javascriptTrackers;
+ expect(trackers).to.be.an('array').with.lengthOf(2);
+ expect(trackers[0]).to.equal('https://existing-tracker.com/t.js');
+ expect(trackers[1]).to.include('data-src=');
+ });
+
+ it('should push viewability.config into existing array jsTrackers', function () {
+ const req = buildNativeReq();
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: JSON.stringify({ native: { title: 'T', javascript_trackers: ['https://tracker1.com/t.js'] } }),
+ ext: {
+ appnexus: {
+ bid_ad_type: 3,
+ viewability: { config: '' }
+ }
}
- }
- ]
+ }]
+ }]
}
- ]
- };
-
- it('should get correct bid response', function () {
- const expectedResponse = [
- {
- 'requestId': '3db3773286ee59',
- 'cpm': 0.5,
- 'creativeId': 29681110,
- 'dealId': undefined,
- 'width': 300,
- 'height': 250,
- 'ad': '',
- 'mediaType': 'banner',
- 'currency': 'USD',
- 'ttl': 300,
- 'netRevenue': true,
- 'adUnitCode': 'code',
- 'mediafuse': {
- 'buyerMemberId': 958
- },
- 'meta': {
- 'dchain': {
- 'ver': '1.0',
- 'complete': 0,
- 'nodes': [{
- 'bsid': '958'
- }]
- }
- }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ const trackers = bids[0].native.javascriptTrackers;
+ expect(trackers).to.be.an('array').with.lengthOf(2);
+ expect(trackers[0]).to.equal('https://tracker1.com/t.js');
+ expect(trackers[1]).to.include('data-src=');
+ });
+
+ it('should combine string jsTracker with eventtrackers into array', function () {
+ const req = buildNativeReq();
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: JSON.stringify({
+ native: {
+ title: 'T',
+ javascript_trackers: 'https://existing-tracker.com/t.js',
+ eventtrackers: [{ method: 1, url: 'https://event-tracker.com/track' }]
+ }
+ }),
+ ext: { appnexus: { bid_ad_type: 3 } }
+ }]
+ }]
}
- ];
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
};
- const result = spec.interpretResponse({ body: response }, {bidderRequest});
- expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0]));
+ const bids = spec.interpretResponse(serverResponse, req);
+ const trackers = bids[0].native.javascriptTrackers;
+ expect(trackers).to.be.an('array').with.lengthOf(2);
+ expect(trackers[0]).to.equal('https://existing-tracker.com/t.js');
+ expect(trackers[1]).to.equal('https://event-tracker.com/track');
});
- it('should reject 0 cpm bids', function () {
- const zeroCpmResponse = deepClone(response);
- zeroCpmResponse.tags[0].ads[0].cpm = 0;
+ it('should push eventtrackers into existing array jsTrackers', function () {
+ const req = buildNativeReq();
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ adm: JSON.stringify({
+ native: {
+ title: 'T',
+ javascript_trackers: ['https://existing-tracker.com/t.js'],
+ eventtrackers: [{ method: 1, url: 'https://event-tracker.com/track' }]
+ }
+ }),
+ ext: { appnexus: { bid_ad_type: 3 } }
+ }]
+ }]
+ }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ const trackers = bids[0].native.javascriptTrackers;
+ expect(trackers).to.be.an('array').with.lengthOf(2);
+ expect(trackers[0]).to.equal('https://existing-tracker.com/t.js');
+ expect(trackers[1]).to.equal('https://event-tracker.com/track');
+ });
+ });
- const bidderRequest = {
- bidderCode: 'mediafuse'
+ // -------------------------------------------------------------------------
+ // getUserSyncs — iframe and pixel syncing
+ // -------------------------------------------------------------------------
+ describe('getUserSyncs - iframe and pixel syncing', function () {
+ it('should add iframe sync when iframeEnabled and purpose-1 consent is present', function () {
+ const syncOptions = { iframeEnabled: true };
+ const gdprConsent = {
+ gdprApplies: true,
+ consentString: 'cs',
+ vendorData: { purpose: { consents: { 1: true } } }
};
+ const syncs = spec.getUserSyncs(syncOptions, [], gdprConsent);
+ expect(syncs).to.have.lengthOf(1);
+ expect(syncs[0].type).to.equal('iframe');
+ expect(syncs[0].url).to.include('gdpr=1');
+ });
- const result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest });
- expect(result.length).to.equal(0);
+ it('should have no gdpr params in pixel url when gdprConsent is null', function () {
+ const syncOptions = { pixelEnabled: true };
+ const serverResponses = [{
+ body: { ext: { appnexus: { userSync: { url: 'https://sync.example.com/px' } } } }
+ }];
+ const syncs = spec.getUserSyncs(syncOptions, serverResponses, null);
+ expect(syncs).to.have.lengthOf(1);
+ expect(syncs[0].url).to.not.include('gdpr');
});
- it('should allow 0 cpm bids if allowZeroCpmBids setConfig is true', function () {
- getGlobal().bidderSettings = {
- mediafuse: {
- allowZeroCpmBids: true
- }
- };
+ it('should append gdpr params with & when pixel url already contains ?', function () {
+ const syncOptions = { pixelEnabled: true };
+ const serverResponses = [{
+ body: { ext: { appnexus: { userSync: { url: 'https://sync.example.com/px?existing=1' } } } }
+ }];
+ const gdprConsent = { gdprApplies: true, consentString: 'cs' };
+ const syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent);
+ expect(syncs[0].url).to.include('existing=1');
+ expect(syncs[0].url).to.include('gdpr=1');
+ expect(syncs[0].url).to.match(/\?existing=1&/);
+ });
+ });
- const zeroCpmResponse = deepClone(response);
- zeroCpmResponse.tags[0].ads[0].cpm = 0;
+ // -------------------------------------------------------------------------
+ // Empty lifecycle methods — onTimeout, onSetTargeting, onAdRenderSucceeded
+ // -------------------------------------------------------------------------
+ describe('empty lifecycle methods', function () {
+ it('onTimeout should not throw', function () {
+ expect(() => spec.onTimeout({ bidderCode: 'mediafuse', timeout: 3000 })).to.not.throw();
+ });
- const bidderRequest = {
- bidderCode: 'mediafuse',
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
- };
+ it('onSetTargeting should not throw', function () {
+ expect(() => spec.onSetTargeting({ adUnitCode: 'au', params: {} })).to.not.throw();
+ });
- const result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest });
- expect(result.length).to.equal(1);
- expect(result[0].cpm).to.equal(0);
+ it('onAdRenderSucceeded should not throw', function () {
+ expect(() => spec.onAdRenderSucceeded({ adUnitCode: 'au' })).to.not.throw();
});
+ });
- it('handles nobid responses', function () {
- const response = {
- 'version': '0.0.1',
- 'tags': [{
- 'uuid': '84ab500420319d',
- 'tag_id': 5976557,
- 'auction_id': '297492697822162468',
- 'nobid': true
- }]
+ // -------------------------------------------------------------------------
+ // onBidWon — string jsTracker
+ // -------------------------------------------------------------------------
+ describe('onBidWon - string jsTracker', function () {
+ it('should handle jsTrackers as a plain string without throwing', function () {
+ const bid = {
+ adId: 'ad-str',
+ adUnitCode: 'unit-str',
+ native: { javascriptTrackers: '//cdn.adnxs.com/v/trk.js?dom_id=%native_dom_id%' }
};
- let bidderRequest;
-
- const result = spec.interpretResponse({ body: response }, {bidderRequest});
- expect(result.length).to.equal(0);
- });
-
- it('handles outstream video responses', function () {
- const response = {
- 'tags': [{
- 'uuid': '84ab500420319d',
- 'ads': [{
- 'ad_type': 'video',
- 'cpm': 0.500000,
- 'notify_url': 'imptracker.com',
- 'rtb': {
- 'video': {
- 'content': ''
- }
- },
- 'javascriptTrackers': ''
- }]
- }]
+ expect(() => spec.onBidWon(bid)).to.not.throw();
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // getUserSyncs — iframeEnabled but consent denied (no iframe added)
+ // -------------------------------------------------------------------------
+ describe('getUserSyncs - iframeEnabled denied by consent', function () {
+ it('should not add iframe sync when iframeEnabled but purpose-1 consent is denied', function () {
+ const syncOptions = { iframeEnabled: true };
+ const gdprConsent = {
+ gdprApplies: true,
+ consentString: 'cs',
+ vendorData: { purpose: { consents: { 1: false } } }
};
- const bidderRequest = {
- bids: [{
- bidId: '84ab500420319d',
- adUnitCode: 'code',
- mediaTypes: {
- video: {
- context: 'outstream'
- }
- }
- }]
- }
+ const syncs = spec.getUserSyncs(syncOptions, [], gdprConsent);
+ expect(syncs).to.have.lengthOf(0);
+ });
- const result = spec.interpretResponse({ body: response }, {bidderRequest});
- expect(result[0]).to.have.property('vastXml');
- expect(result[0]).to.have.property('vastImpUrl');
- expect(result[0]).to.have.property('mediaType', 'video');
- });
-
- it('handles instream video responses', function () {
- const response = {
- 'tags': [{
- 'uuid': '84ab500420319d',
- 'ads': [{
- 'ad_type': 'video',
- 'cpm': 0.500000,
- 'notify_url': 'imptracker.com',
- 'rtb': {
- 'video': {
- 'asset_url': 'https://sample.vastURL.com/here/vid'
- }
- },
- 'javascriptTrackers': ''
+ it('should not add pixel sync when serverResponses is empty', function () {
+ const syncOptions = { pixelEnabled: true };
+ const syncs = spec.getUserSyncs(syncOptions, [], null);
+ expect(syncs).to.have.lengthOf(0);
+ });
+
+ it('should not add pixel sync when response has no userSync url', function () {
+ const syncOptions = { pixelEnabled: true };
+ const serverResponses = [{ body: { ext: { appnexus: {} } } }];
+ const syncs = spec.getUserSyncs(syncOptions, serverResponses, null);
+ expect(syncs).to.have.lengthOf(0);
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // interpretResponse — bid_ad_type not in RESPONSE_MEDIA_TYPE_MAP
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - unknown bid_ad_type', function () {
+ it('should not throw when bid_ad_type=2 is not in the media type map', function () {
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.5,
+ adm: 'creative
',
+ ext: { appnexus: { bid_ad_type: 2 } }
+ }]
}]
- }]
+ }
};
- const bidderRequest = {
- bids: [{
- bidId: '84ab500420319d',
- adUnitCode: 'code',
- mediaTypes: {
- video: {
- context: 'instream'
- }
- }
- }]
- }
+ expect(() => spec.interpretResponse(serverResponse, req)).to.not.throw();
+ });
+ });
- const result = spec.interpretResponse({ body: response }, {bidderRequest});
- expect(result[0]).to.have.property('vastUrl');
- expect(result[0]).to.have.property('vastImpUrl');
- expect(result[0]).to.have.property('mediaType', 'video');
- });
-
- it('handles adpod responses', function () {
- const response = {
- 'tags': [{
- 'uuid': '84ab500420319d',
- 'ads': [{
- 'ad_type': 'video',
- 'brand_category_id': 10,
- 'cpm': 0.500000,
- 'notify_url': 'imptracker.com',
- 'rtb': {
- 'video': {
- 'asset_url': 'https://sample.vastURL.com/here/adpod',
- 'duration_ms': 30000,
+ // -------------------------------------------------------------------------
+ // interpretResponse — adpod brand_category_id not in APPNEXUS_CATEGORY_MAPPING
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - adpod brand_category_id not in mapping', function () {
+ it('should not set primaryCatId when brand_category_id has no mapping entry', function () {
+ const bid = {
+ bidder: 'mediafuse',
+ adUnitCode: 'pod',
+ bidId: 'pod-bid',
+ mediaTypes: { video: { context: 'adpod', adPodDurationSec: 30, durationRangeSec: [15] } },
+ params: { placementId: 111 }
+ };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 2.0,
+ ext: {
+ appnexus: {
+ bid_ad_type: 1,
+ brand_category_id: 99999
+ }
}
- },
- 'viewability': {
- 'config': ''
- }
+ }]
}]
- }]
+ }
};
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].meta && bids[0].meta.primaryCatId).to.be.undefined;
+ });
+ });
- const bidderRequest = {
- bids: [{
- bidId: '84ab500420319d',
- adUnitCode: 'code',
- mediaTypes: {
- video: {
- context: 'adpod'
- }
- }
- }]
+ // -------------------------------------------------------------------------
+ // buildRequests — topmostLocation falsy → rd_ref=''
+ // -------------------------------------------------------------------------
+ describe('buildRequests - topmostLocation falsy', function () {
+ it('should set rd_ref to empty string when topmostLocation is not present', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.refererInfo = {
+ topmostLocation: null,
+ reachedTop: false,
+ numIframes: 0,
+ stack: []
};
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.ext.appnexus.referrer_detection.rd_ref).to.equal('');
+ });
+ });
- const result = spec.interpretResponse({ body: response }, {bidderRequest});
- expect(result[0]).to.have.property('vastUrl');
- expect(result[0].video.context).to.equal('adpod');
- expect(result[0].video.durationSeconds).to.equal(30);
- });
-
- it('handles native responses', function () {
- const response1 = deepClone(response);
- response1.tags[0].ads[0].ad_type = 'native';
- response1.tags[0].ads[0].rtb.native = {
- 'title': 'Native Creative',
- 'desc': 'Cool description great stuff',
- 'desc2': 'Additional body text',
- 'ctatext': 'Do it',
- 'sponsored': 'MediaFuse',
- 'icon': {
- 'width': 0,
- 'height': 0,
- 'url': 'https://cdn.adnxs.com/icon.png'
- },
- 'main_img': {
- 'width': 2352,
- 'height': 1516,
- 'url': 'https://cdn.adnxs.com/img.png'
- },
- 'link': {
- 'url': 'https://www.mediafuse.com',
- 'fallback_url': '',
- 'click_trackers': ['https://nym1-ib.adnxs.com/click']
- },
- 'impression_trackers': ['https://example.com'],
- 'rating': '5',
- 'displayurl': 'https://mediafuse.com/?url=display_url',
- 'likes': '38908320',
- 'downloads': '874983',
- 'price': '9.99',
- 'saleprice': 'FREE',
- 'phone': '1234567890',
- 'address': '28 W 23rd St, New York, NY 10010',
- 'privacy_link': 'https://www.mediafuse.com/privacy-policy-agreement/',
- 'javascriptTrackers': ''
+ // -------------------------------------------------------------------------
+ // buildRequests — addtlConsent all-NaN → addtl_consent not set
+ // -------------------------------------------------------------------------
+ describe('buildRequests - addtlConsent all-NaN values', function () {
+ it('should not set addtl_consent when all values after ~ are NaN', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.gdprConsent = {
+ gdprApplies: true,
+ consentString: 'cs',
+ addtlConsent: '1~abc.def.ghi'
};
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.user && req.data.user.ext && req.data.user.ext.addtl_consent).to.be.undefined;
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // buildRequests — EID with unrecognized source passes through unchanged
+ // -------------------------------------------------------------------------
+ describe('buildRequests - EID unrecognized source', function () {
+ it('should pass through EID unchanged when source is neither adserver.org nor uidapi.com', function () {
+ const bid = deepClone(BASE_BID);
+ bid.userIdAsEids = [{ source: 'unknown-id-provider.com', uids: [{ id: 'some-id', atype: 1 }] }];
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const eid = req.data.user && req.data.user.ext && req.data.user.ext.eids &&
+ req.data.user.ext.eids.find(e => e.source === 'unknown-id-provider.com');
+ if (eid) {
+ expect(eid.rti_partner).to.be.undefined;
}
+ });
+ });
- const result = spec.interpretResponse({ body: response1 }, {bidderRequest});
- expect(result[0].native.title).to.equal('Native Creative');
- expect(result[0].native.body).to.equal('Cool description great stuff');
- expect(result[0].native.cta).to.equal('Do it');
- expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png');
- });
-
- it('supports configuring outstream renderers', function () {
- const outstreamResponse = deepClone(response);
- outstreamResponse.tags[0].ads[0].rtb.video = {};
- outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js';
-
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- renderer: {
- options: {
- adText: 'configured'
- }
- },
- mediaTypes: {
- video: {
- context: 'outstream'
- }
- }
- }]
+ // -------------------------------------------------------------------------
+ // getBidFloor — edge cases
+ // -------------------------------------------------------------------------
+ describe('buildRequests - getBidFloor edge cases', function () {
+ it('should return null when getFloor returns a non-plain-object (null)', function () {
+ const bid = deepClone(BASE_BID);
+ bid.getFloor = () => null;
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].bidfloor).to.be.undefined;
+ });
+
+ it('should return null when getFloor returns a NaN floor value', function () {
+ const bid = deepClone(BASE_BID);
+ bid.getFloor = () => ({ currency: 'USD', floor: NaN });
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].bidfloor).to.be.undefined;
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // buildRequests — banner_frameworks type guard
+ // -------------------------------------------------------------------------
+ describe('buildRequests - banner_frameworks invalid type', function () {
+ it('should not set banner_frameworks when value is a string (not array of nums)', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { banner: { sizes: [[300, 250]] } };
+ bid.params.banner_frameworks = 'not-an-array';
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.banner_frameworks).to.be.undefined;
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // onBidWon — no trk.js in jsTrackers
+ // -------------------------------------------------------------------------
+ describe('onBidWon - jsTrackers with no trk.js URL', function () {
+ it('should return early when no trk.js tracker is found in jsTrackers array', function () {
+ const bid = {
+ adId: 'ad-no-trk',
+ adUnitCode: 'unit-no-trk',
+ native: { javascriptTrackers: ['https://other-tracker.com/track.js', 'https://another.com/t.js'] }
};
+ expect(() => spec.onBidWon(bid)).to.not.throw();
+ });
+ });
- const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest});
- expect(result[0].renderer.config).to.deep.equal(
- bidderRequest.bids[0].renderer.options
- );
- });
-
- it('should add deal_priority and deal_code', function() {
- const responseWithDeal = deepClone(response);
- responseWithDeal.tags[0].ads[0].ad_type = 'video';
- responseWithDeal.tags[0].ads[0].deal_priority = 5;
- responseWithDeal.tags[0].ads[0].deal_code = '123';
- responseWithDeal.tags[0].ads[0].rtb.video = {
- duration_ms: 1500,
- player_width: 640,
- player_height: 340,
+ // -------------------------------------------------------------------------
+ // onBidWon — HTML-snippet tracker
+ // -------------------------------------------------------------------------
+ describe('onBidWon - HTML-snippet style tracker', function () {
+ it('should extract src from HTML-snippet style tracker without throwing', function () {
+ const bid = {
+ adId: 'ad-html',
+ adUnitCode: 'unit-html',
+ native: {
+ javascriptTrackers: ['']
+ }
};
+ expect(() => spec.onBidWon(bid)).to.not.throw();
+ });
+ });
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code',
- mediaTypes: {
- video: {
- context: 'adpod'
- }
- }
- }]
- }
- const result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest});
- expect(Object.keys(result[0].mediafuse)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']);
- expect(result[0].video.dealTier).to.equal(5);
+ // -------------------------------------------------------------------------
+ // buildRequests — video params frameworks type guard
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video params frameworks', function () {
+ it('should not set video_frameworks when params.video.frameworks is not an array', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480] } };
+ bid.params.video = { frameworks: 'not-an-array' };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.video_frameworks).to.be.undefined;
});
+ });
- it('should add advertiser id', function() {
- const responseAdvertiserId = deepClone(response);
- responseAdvertiserId.tags[0].ads[0].advertiser_id = '123';
+ // -------------------------------------------------------------------------
+ // buildRequests — banner_frameworks param fallback
+ // -------------------------------------------------------------------------
+ describe('buildRequests - banner frameworks param fallback', function () {
+ it('should use bid.params.frameworks as fallback when banner_frameworks is absent', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { banner: { sizes: [[300, 250]] } };
+ bid.params.frameworks = [1, 2];
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].ext.appnexus.banner_frameworks).to.deep.equal([1, 2]);
+ });
+ });
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
- }
- const result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest});
- expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']);
+ // -------------------------------------------------------------------------
+ // buildRequests — refererInfo.stack absent
+ // -------------------------------------------------------------------------
+ describe('buildRequests - refererInfo stack absent', function () {
+ it('should set rd_stk to undefined when stack is not present in refererInfo', function () {
+ const bidderRequest = deepClone(BASE_BIDDER_REQUEST);
+ bidderRequest.refererInfo = {
+ topmostLocation: 'http://example.com',
+ reachedTop: true,
+ numIframes: 0
+ };
+ const [req] = spec.buildRequests([deepClone(BASE_BID)], bidderRequest);
+ expect(req.data.ext.appnexus.referrer_detection.rd_stk).to.be.undefined;
});
+ });
- it('should add brand id', function() {
- const responseBrandId = deepClone(response);
- responseBrandId.tags[0].ads[0].brand_id = 123;
+ // -------------------------------------------------------------------------
+ // interpretResponse — renderer options from mediaTypes.video.renderer.options
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - renderer options from mediaTypes.video.renderer', function () {
+ it('should use mediaTypes.video.renderer.options when available', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'outstream', playerSize: [640, 480], renderer: { options: { key: 'val' } } } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 3.0,
+ ext: {
+ appnexus: {
+ bid_ad_type: 1,
+ renderer_url: 'https://cdn.adnxs.com/renderer.js',
+ renderer_id: 42
+ }
+ }
+ }]
+ }]
+ }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].renderer).to.exist;
+ });
+ });
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
- }
- const result = spec.interpretResponse({ body: responseBrandId }, {bidderRequest});
- expect(Object.keys(result[0].meta)).to.include.members(['brandId']);
+ // -------------------------------------------------------------------------
+ // buildRequests — video params.video takes priority over mediaTypes.video for maxduration
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video maxduration skip overwrite', function () {
+ it('should not overwrite maxduration set by params.video when mediaTypes.video.maxduration also present', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], maxduration: 15 } };
+ bid.params.video = { maxduration: 30 };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].video.maxduration).to.equal(30);
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // buildRequests — video params.video takes priority over mediaTypes.video for skippable
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video skippable skip overwrite', function () {
+ it('should not overwrite skippable set by params.video when mediaTypes.video.skip is also present', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], skip: 1 } };
+ bid.params.video = { skippable: false };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].video.skippable).to.be.false;
});
+ });
- it('should add advertiserDomains', function() {
- const responseAdvertiserId = deepClone(response);
- responseAdvertiserId.tags[0].ads[0].adomain = ['123'];
+ // -------------------------------------------------------------------------
+ // buildRequests — video params.video takes priority over mediaTypes.video for skipoffset
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video skipoffset skip overwrite', function () {
+ it('should not overwrite skipoffset set by params.video when mediaTypes.video.skipafter is also present', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], skipafter: 10 } };
+ bid.params.video = { skipoffset: 5 };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].video.skipoffset).to.equal(5);
+ });
+ });
- const bidderRequest = {
- bids: [{
- bidId: '3db3773286ee59',
- adUnitCode: 'code'
- }]
- }
- const result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest});
- expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']);
- expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]);
+ // -------------------------------------------------------------------------
+ // buildRequests — video playbackmethod type guard
+ // -------------------------------------------------------------------------
+ describe('buildRequests - video playbackmethod', function () {
+ it('should not set playback_method when mediaTypes.video.playbackmethod is not an array', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480], playbackmethod: 2 } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ expect(req.data.imp[0].video.playback_method).to.be.undefined;
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ // interpretResponse — video nurl without asset_url
+ // -------------------------------------------------------------------------
+ describe('interpretResponse - video nurl without asset_url', function () {
+ it('should set vastImpUrl but not vastUrl when nurl present but asset_url absent', function () {
+ const bid = deepClone(BASE_BID);
+ bid.mediaTypes = { video: { context: 'instream', playerSize: [640, 480] } };
+ const [req] = spec.buildRequests([bid], deepClone(BASE_BIDDER_REQUEST));
+ const impId = req.data.imp[0].id;
+
+ const serverResponse = {
+ body: {
+ seatbid: [{
+ bid: [{
+ impid: impId,
+ price: 1.0,
+ nurl: 'https://notify.example.com/win',
+ ext: {
+ appnexus: {
+ bid_ad_type: 1
+ // no asset_url, no renderer_url/renderer_id
+ }
+ }
+ }]
+ }]
+ }
+ };
+ const bids = spec.interpretResponse(serverResponse, req);
+ expect(bids[0].vastImpUrl).to.equal('https://notify.example.com/win');
+ expect(bids[0].vastUrl).to.not.include('&redir=');
});
});
});