From a9de3c15ac9a108b43a1e2df04abd6dfb5297530 Mon Sep 17 00:00:00 2001 From: dev-adverxo Date: Fri, 15 Nov 2024 20:20:51 +0100 Subject: [PATCH] New bid adapter: Adverxo (#12376) * init adverxo adapter * Add support for user sync * Make host parameter optional Introduce new aliases and an auction url for each. * Set skipPbsAliasing=true for aliases We will also have the aliseses in pbs. * Add new alias --- modules/adverxoBidAdapter.js | 356 ++++++++++ modules/adverxoBidAdapter.md | 101 +++ test/spec/modules/adverxoBidAdapter_spec.js | 724 ++++++++++++++++++++ 3 files changed, 1181 insertions(+) create mode 100644 modules/adverxoBidAdapter.js create mode 100644 modules/adverxoBidAdapter.md create mode 100644 test/spec/modules/adverxoBidAdapter_spec.js diff --git a/modules/adverxoBidAdapter.js b/modules/adverxoBidAdapter.js new file mode 100644 index 00000000000..f293d7e01a6 --- /dev/null +++ b/modules/adverxoBidAdapter.js @@ -0,0 +1,356 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {ortbConverter as OrtbConverter} from '../libraries/ortbConverter/converter.js'; +import {Renderer} from '../src/Renderer.js'; +import {deepAccess, deepSetValue} from '../src/utils.js'; +import {config} from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/auction.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const BIDDER_CODE = 'adverxo'; + +const ALIASES = [ + {code: 'adport', skipPbsAliasing: true}, + {code: 'bidsmind', skipPbsAliasing: true}, + {code: 'mobupps', skipPbsAliasing: true} +]; + +const AUCTION_URLS = { + adverxo: 'js.pbsadverxo.com', + adport: 'diclotrans.com', + bidsmind: 'egrevirda.com', + mobupps: 'traffhb.com' +}; + +const ENDPOINT_URL_AD_UNIT_PLACEHOLDER = '{AD_UNIT}'; +const ENDPOINT_URL_AUTH_PLACEHOLDER = '{AUTH}'; +const ENDPOINT_URL_HOST_PLACEHOLDER = '{HOST}'; + +const ENDPOINT_URL = `https://${ENDPOINT_URL_HOST_PLACEHOLDER}/pickpbs?id=${ENDPOINT_URL_AD_UNIT_PLACEHOLDER}&auth=${ENDPOINT_URL_AUTH_PLACEHOLDER}`; + +const ORTB_MTYPES = { + 1: BANNER, + 2: VIDEO, + 4: NATIVE +}; + +const USYNC_TYPES = { + IFRAME: 'iframe', + REDIRECT: 'image' +}; + +const DEFAULT_CURRENCY = 'USD'; + +const ortbConverter = OrtbConverter({ + context: { + netRevenue: true, + ttl: 60, + }, + request: function request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + utils.deepSetValue(request, 'device.ip', 'caller'); + utils.deepSetValue(request, 'ext.avx_add_vast_url', 1); + + const eids = deepAccess(bidderRequest, 'bids.0.userIdAsEids'); + + if (eids && eids.length) { + deepSetValue(request, 'user.ext.eids', eids); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floor = adverxoUtils.getBidFloor(bidRequest); + + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; + }, + bidResponse: function (buildBidResponse, bid, context) { + bid.adm = bid.adm.replaceAll(`\${AUCTION_PRICE}`, bid.price); + + if (FEATURES.NATIVE && ORTB_MTYPES[bid.mtype] === NATIVE) { + if (typeof bid?.adm === 'string') { + bid.adm = JSON.parse(bid.adm); + } + + if (bid?.adm?.native) { + bid.adm = bid.adm.native; + } + } + + const result = buildBidResponse(bid, context); + + if (FEATURES.VIDEO) { + if (bid?.ext?.avx_vast_url) { + result.vastUrl = bid.ext.avx_vast_url; + } + + if (bid?.ext?.avx_video_renderer_url) { + result.avxVideoRendererUrl = bid.ext.avx_video_renderer_url; + } + } + + return result; + } +}); + +const userSyncUtils = { + buildUsyncParams: function (gdprConsent, uspConsent, gppConsent) { + const params = []; + + if (gdprConsent) { + params.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + params.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + + if (config.getConfig('coppa') === true) { + params.push('coppa=1'); + } + + if (uspConsent) { + params.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + params.push('gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(','))); + } + + return params.length ? params.join('&') : ''; + } +}; + +const videoUtils = { + createOutstreamVideoRenderer: function (bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: bid.avxVideoRendererUrl, + loaded: false, + adUnitCode: bid.adUnitCode + }); + + try { + renderer.setRender(this.outstreamRender.bind(this)); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; + }, + + outstreamRender: function (bid, doc) { + bid.renderer.push(() => { + const win = (doc) ? doc.defaultView : window; + + win.adxVideoRenderer.renderAd({ + targetId: bid.adUnitCode, + adResponse: {content: bid.vastXml} + }); + }); + } +}; + +const adverxoUtils = { + buildAuctionUrl: function (bidderCode, host, adUnitId, adUnitAuth) { + const auctionUrl = host || AUCTION_URLS[bidderCode]; + + return ENDPOINT_URL + .replace(ENDPOINT_URL_HOST_PLACEHOLDER, auctionUrl) + .replace(ENDPOINT_URL_AD_UNIT_PLACEHOLDER, adUnitId) + .replace(ENDPOINT_URL_AUTH_PLACEHOLDER, adUnitAuth); + }, + + groupBidRequestsByAdUnit: function (bidRequests) { + const groupedBidRequests = new Map(); + + bidRequests.forEach(bidRequest => { + const adUnit = { + host: bidRequest.params.host, + id: bidRequest.params.adUnitId, + auth: bidRequest.params.auth, + }; + + if (!groupedBidRequests.get(adUnit)) { + groupedBidRequests.set(adUnit, []); + } + + groupedBidRequests.get(adUnit).push(bidRequest); + }); + + return groupedBidRequests; + }, + + getBidFloor: function (bid) { + if (utils.isFn(bid.getFloor)) { + const floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + + if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { + return floor.floor; + } + } + + return null; + } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + aliases: ALIASES, + + /** + * Determines whether the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return {boolean} True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (!utils.isPlainObject(bid.params) || !Object.keys(bid.params).length) { + utils.logWarn('Adverxo Bid Adapter: bid params must be provided.'); + return false; + } + + if (!bid.params.adUnitId || typeof bid.params.adUnitId !== 'number') { + utils.logWarn('Adverxo Bid Adapter: adUnitId bid param is required and must be a number'); + return false; + } + + if (!bid.params.auth || typeof bid.params.auth !== 'string') { + utils.logWarn('Adverxo Bid Adapter: auth bid param is required and must be a string'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest an array of bids + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const result = []; + + const bidRequestsByAdUnit = adverxoUtils.groupBidRequestsByAdUnit(validBidRequests); + + bidRequestsByAdUnit.forEach((adUnitBidRequests, adUnit) => { + const ortbRequest = ortbConverter.toORTB({ + bidRequests: adUnitBidRequests, + bidderRequest + }); + + result.push({ + method: 'POST', + url: adverxoUtils.buildAuctionUrl(bidderRequest.bidderCode, adUnit.host, adUnit.id, adUnit.auth), + data: ortbRequest, + bids: adUnitBidRequests + }); + }); + + return result; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest Adverxo bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse || !bidRequest) { + return []; + } + + const bids = ortbConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids.map((bid) => { + const thisRequest = utils.getBidRequest(bid.requestId, [bidRequest]); + const context = utils.deepAccess(thisRequest, 'mediaTypes.video.context'); + + if (FEATURES.VIDEO && bid.mediaType === 'video' && context === 'outstream') { + bid.renderer = videoUtils.createOutstreamVideoRenderer(bid); + } + + return bid; + }); + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} responses List of server's responses. + * @param {*} gdprConsent + * @param {*} uspConsent + * @param {*} gppConsent + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { + if (!responses || responses.length === 0 || (!syncOptions.pixelEnabled && !syncOptions.iframeEnabled)) { + return []; + } + + const privacyParams = userSyncUtils.buildUsyncParams(gdprConsent, uspConsent, gppConsent); + const syncType = syncOptions.iframeEnabled ? USYNC_TYPES.IFRAME : USYNC_TYPES.REDIRECT; + + const result = []; + + for (const response of responses) { + const syncUrls = response.body?.ext?.avx_usync; + + if (!syncUrls || syncUrls.length === 0) { + continue; + } + + for (const url of syncUrls) { + let finalUrl = url; + + if (!finalUrl.includes('?')) { + finalUrl += '?'; + } else { + finalUrl += '&'; + } + + finalUrl += 'type=' + syncType; + + if (privacyParams.length !== 0) { + finalUrl += `&${privacyParams}`; + } + + result.push({ + type: syncType, + url: finalUrl + }); + } + } + + return result; + } +} + +registerBidder(spec); diff --git a/modules/adverxoBidAdapter.md b/modules/adverxoBidAdapter.md new file mode 100644 index 00000000000..ae6072d2738 --- /dev/null +++ b/modules/adverxoBidAdapter.md @@ -0,0 +1,101 @@ +# Overview + +``` +Module Name: Adverxo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: developer@adverxo.com +``` + +# Description + +Module that connects to Adverxo to fetch bids. +Banner, native and video formats are supported. + +# Bid Parameters + +| Name | Required? | Description | Example | Type | +|------------|-----------|-------------------------------------------------------------------|----------------------------------------------|-----------| +| `host` | No | Ad network host. | `prebidTest.adverxo.com` | `String` | +| `adUnitId` | Yes | Unique identifier for the ad unit in Adverxo platform. | `413` | `Integer` | +| `auth` | Yes | Authentication token provided by Adverxo platform for the AdUnit. | `'61336d75e414c77c367ce5c47c2599ce80a8x32b'` | `String` | + +# Test Parameters + +```javascript +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [400, 300], + [320, 50] + ] + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 1, + auth: '61336e753414c77c367deac47c2595ce80a8032b' + } + }] + }, + { + code: 'native-ad-div', + mediaTypes: { + native: { + image: { + required: true, + sizes: [400, 300] + }, + title: { + required: true, + len: 75 + }, + body: { + required: false, + len: 200 + }, + cta: { + required: false, + len: 75 + }, + sponsoredBy: { + required: false + } + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 2, + auth: '9a640dfccc3381e71fxc29ffd4a72wabd29g9d86' + } + }] + }, + { + code: 'video-div', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + maxduration: 30, + skip: 1 + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 3, + auth: '1ax23d9621f21da28a2eab6f79bd5fbcf4d037c1' + } + }] + } +]; + +``` diff --git a/test/spec/modules/adverxoBidAdapter_spec.js b/test/spec/modules/adverxoBidAdapter_spec.js new file mode 100644 index 00000000000..17ea33f8e67 --- /dev/null +++ b/test/spec/modules/adverxoBidAdapter_spec.js @@ -0,0 +1,724 @@ +import {expect} from 'chai'; +import {spec} from 'modules/adverxoBidAdapter.js'; +import {config} from 'src/config'; + +describe('Adverxo Bid Adapter', () => { + function makeBidRequestWithParams(params) { + return { + bidId: '2e9f38ea93bb9e', + bidder: 'adverxo', + adUnitCode: 'adunit-code', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: params, + bidderRequestId: 'test-bidder-request-id' + }; + } + + const bannerBidRequests = [ + { + bidId: 'bid-banner', + bidder: 'adverxo', + adUnitCode: 'adunit-code', + userIdAsEids: [{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }], + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample', + }, + bidderRequestId: 'test-bidder-request-id', + }, + ]; + + const bannerBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: bannerBidRequests, + auctionId: 'new-auction-id' + }; + + const nativeOrtbRequest = { + assets: [ + { + id: 1, + required: 1, + img: {type: 3, w: 150, h: 50} + }, + { + id: 2, + required: 1, + title: {len: 80} + }, + { + id: 3, + required: 0, + data: {type: 1} + } + ] + }; + + const nativeBidRequests = [ + { + bidId: 'bid-native', + mediaTypes: { + native: { + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + }, + ]; + + const nativeBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: nativeBidRequests, + auctionId: 'new-auction-id' + }; + + const videoInstreamBidRequests = [ + { + bidId: 'bid-video', + mediaTypes: { + video: { + context: 'instream', + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + } + ]; + + const videoInstreamBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: videoInstreamBidRequests, + auctionId: 'new-auction-id' + }; + + const videoOutstreamBidRequests = [ + { + bidId: 'bid-video', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + } + ]; + + const videoOutstreamBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: videoOutstreamBidRequests, + auctionId: 'new-auction-id' + }; + + afterEach(function () { + config.resetConfig(); + }); + + describe('isBidRequestValid', function () { + it('should validate bid request with valid params', () => { + const validBid = makeBidRequestWithParams({ + adUnitId: 1, + auth: 'authExample', + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.be.true; + }); + + it('should not validate bid request with empty params', () => { + const invalidBid = makeBidRequestWithParams({}); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should not validate bid request with missing param(adUnitId)', () => { + const invalidBid = makeBidRequestWithParams({ + auth: 'authExample', + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should not validate bid request with missing param(auth)', () => { + const invalidBid = makeBidRequestWithParams({ + adUnitId: 1, + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should validate bid request with missing param(host)', () => { + const invalidBid = makeBidRequestWithParams({ + adUnitId: 1, + auth: 'authExample', + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.true; + }); + }); + + describe('buildRequests', () => { + it('should add eids information to the request', function () { + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + + expect(request.data.user.ext.eids).to.exist; + expect(request.data.user.ext.eids).to.deep.equal(bannerBidRequests[0].userIdAsEids); + }); + + it('should use correct bidUrl for an alias', () => { + const bidRequests = [ + { + bidder: 'bidsmind', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + adUnitId: 1, + auth: 'authExample', + } + }, + ]; + + const bidderRequest = { + bidderCode: 'bidsmind', + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://egrevirda.com/pickpbs?id=1&auth=authExample'); + }); + + it('should use correct default bidUrl', () => { + const bidRequests = [ + { + bidder: 'adverxo', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + adUnitId: 1, + auth: 'authExample', + } + }, + ]; + + const bidderRequest = { + bidderCode: 'adverxo', + bids: bidRequests + }; + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://js.pbsadverxo.com/pickpbs?id=1&auth=authExample'); + }); + + it('should build post request for banner', () => { + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + expect(request.data.device.ip).to.equal('caller'); + expect(request.data.ext.avx_add_vast_url).to.equal(1); + }); + + if (FEATURES.NATIVE) { + it('should build post request for native', () => { + const request = spec.buildRequests(nativeBidRequests, nativeBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + + const nativeRequest = JSON.parse(request.data.imp[0]['native'].request); + + expect(nativeRequest.assets).to.have.lengthOf(3); + + expect(nativeRequest.assets[0]).to.deep.equal({ + id: 1, + required: 1, + img: {w: 150, h: 50, type: 3} + }); + + expect(nativeRequest.assets[1]).to.deep.equal({ + id: 2, + required: 1, + title: {len: 80} + }); + + expect(nativeRequest.assets[2]).to.deep.equal({ + id: 3, + required: 0, + data: {type: 1} + }); + }); + } + + if (FEATURES.VIDEO) { + it('should build post request for video', function () { + const request = spec.buildRequests(videoInstreamBidRequests, videoInstreamBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + + const ortbRequest = request.data; + + expect(ortbRequest.imp).to.have.lengthOf(1); + + expect(ortbRequest.imp[0]).to.deep.equal({ + id: 'bid-video', + video: { + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }); + }); + } + + it('should add bid floor to request', function () { + const bannerBidRequestWithFloor = { + ...bannerBidRequests[0], + getFloor: () => ({currency: 'USD', floor: 3}) + }; + + const request = spec.buildRequests([bannerBidRequestWithFloor], {})[0].data; + + expect(request.imp[0].bidfloor).to.equal(3); + expect(request.imp[0].bidfloorcur).to.equal('USD'); + }); + }); + + describe('interpretResponse', () => { + it('should return empty array if serverResponse is not defined', () => { + const bidRequest = spec.buildRequests(bannerBidRequests, bannerBidderRequest); + const bids = spec.interpretResponse(undefined, bidRequest); + + expect(bids.length).to.equal(0); + }); + + it('should interpret banner response', () => { + const bidResponse = { + body: { + id: 'bid-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-banner', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 1, + adomain: ['test.com'], + adm: '
' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'banner', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + requestId: 'bid-banner', + ttl: 60, + width: 300, + ad: '
' + }, + ]; + + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + + it('should replace openrtb auction price macro', () => { + const bidResponse = { + body: { + id: 'bid-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-banner', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 1, + adomain: ['test.com'], + adm: '' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids[0].ad).to.equal(''); + }); + + if (FEATURES.NATIVE) { + it('should interpret native response', () => { + const bidResponse = { + body: { + id: 'native-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-native', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 4, + adomain: ['test.com'], + adm: '{"native":{"assets":[{"id":2,"title":{"text":"Title"}},{"id":3,"data":{"value":"Description"}},{"id":1,"img":{"url":"http://example.com?img","w":150,"h":50}}],"link":{"url":"http://example.com?link"}}}' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'native', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + requestId: 'bid-native', + ttl: 60, + width: 300, + native: { + ortb: { + assets: [ + {id: 2, title: {text: 'Title'}}, + {id: 3, data: {value: 'Description'}}, + {id: 1, img: {url: 'http://example.com?img', w: 150, h: 50}} + ], + link: {url: 'http://example.com?link'} + } + } + } + ]; + + const request = spec.buildRequests(nativeBidRequests, nativeBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + } + + if (FEATURES.VIDEO) { + it('should interpret video instream response', () => { + const bidResponse = { + body: { + id: 'video-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-video', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 2, + adomain: ['test.com'], + adm: 'vastXml', + ext: { + avx_vast_url: 'vastUrl' + } + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'video', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + playerHeight: 300, + playerWidth: 400, + requestId: 'bid-video', + ttl: 60, + vastUrl: 'vastUrl', + vastXml: 'vastXml', + width: 300 + } + ]; + + const request = spec.buildRequests(videoInstreamBidRequests, videoInstreamBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + + it('should interpret video outstream response', () => { + const bidResponse = { + body: { + id: 'video-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-video', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 2, + adomain: ['test.com'], + adm: 'vastXml', + ext: { + avx_vast_url: 'vastUrl', + avx_video_renderer_url: 'videoRendererUrl', + } + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + avxVideoRendererUrl: 'videoRendererUrl', + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'video', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + playerHeight: 300, + playerWidth: 400, + requestId: 'bid-video', + ttl: 60, + vastUrl: 'vastUrl', + vastXml: 'vastXml', + width: 300 + } + ]; + + const request = spec.buildRequests(videoOutstreamBidRequests, videoOutstreamBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids[0].renderer.url).to.equal('videoRendererUrl'); + + delete (bids[0].renderer); + expect(bids).to.deep.equal(expectedBids); + }); + } + }); + + describe('getUserSyncs', () => { + const exampleUrl = 'https://example.com/usync?id=5'; + const iframeConfig = {iframeEnabled: true}; + + const responses = [{ + body: {ext: {avx_usync: [exampleUrl]}} + }]; + + const responseWithoutQueryString = [{ + body: {ext: {avx_usync: ['https://example.com/usync/sf/5']}} + }]; + + it('should not return empty list if not allowed', function () { + expect(spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, responses, undefined, undefined, undefined)).to.be.empty; + }); + + it('should not return iframe if not allowed', function () { + expect(spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, responses, undefined, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${exampleUrl}&type=image` + }]); + }); + + it('should add query string to url when missing', function () { + expect(spec.getUserSyncs(iframeConfig, responseWithoutQueryString, undefined, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `https://example.com/usync/sf/5?type=iframe` + }]); + }); + + it('should not add parameters if not provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, undefined, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should add GDPR parameters if provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, {gdprApplies: true}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(iframeConfig, responses, + {gdprApplies: true, consentString: 'foo?'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=1&gdpr_consent=foo%3F` + }]); + + expect(spec.getUserSyncs(iframeConfig, responses, + {gdprApplies: false, consentString: 'bar'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, undefined, 'foo?', undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&us_privacy=foo%3F` + }]); + }); + + it('should not apply if not gppConsent.gppString', function () { + const gppConsent = {gppString: '', applicableSections: [123]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function () { + const gppConsent = {gppString: '', applicableSections: undefined}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function () { + const gppConsent = {gppString: '', applicableSections: []}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should apply if all above are available', function () { + const gppConsent = {gppString: 'foo?', applicableSections: [123]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function () { + const gppConsent = {gppString: 'foo', applicableSections: [123, 456]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); +});