diff --git a/modules/adtrgtmeBidAdapter.js b/modules/adtrgtmeBidAdapter.js new file mode 100644 index 00000000000..4dc95ce6bc1 --- /dev/null +++ b/modules/adtrgtmeBidAdapter.js @@ -0,0 +1,333 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { deepAccess, isFn, isStr, isNumber, isArray, isEmpty, isPlainObject, generateUUID, logWarn } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; + +const INTEGRATION_METHOD = 'prebid.js'; +const BIDDER_CODE = 'adtrgtme'; +const ENDPOINT = 'https://z.cdn.adtarget.market/ssp?prebid&s='; +const ADAPTER_VERSION = '1.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; + +function transformSizes(sizes) { + const getSize = (size) => { + return { + w: parseInt(size[0]), + h: parseInt(size[1]) + } + } + if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) { + return [ getSize(sizes) ]; + } + return sizes.map(getSize); +} + +function extractUserSyncUrls(syncOptions, pixels) { + let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi; + let tagNameRegExp = /\w*(?=\s)/; + let srcRegExp = /src=("|')(.*?)\1/; + let userSyncObjects = []; + + if (pixels) { + let matchedItems = pixels.match(itemsRegExp); + if (matchedItems) { + matchedItems.forEach(item => { + let tagName = item.match(tagNameRegExp)[0]; + let url = item.match(srcRegExp)[2]; + if (tagName && url) { + let tagType = tagName.toLowerCase() === 'img' ? 'image' : 'iframe'; + if ((!syncOptions.iframeEnabled && tagType === 'iframe') || + (!syncOptions.pixelEnabled && tagType === 'image')) { + return; + } + userSyncObjects.push({ + type: tagType, + url: url + }); + } + }); + } + } + return userSyncObjects; +} + +function isSecure(bid) { + return deepAccess(bid, 'params.bidOverride.imp.secure') || (document.location.protocol === 'https:') ? 1 : 0; +}; + +function getMediaType(bid) { + return deepAccess(bid, 'mediaTypes.banner') ? BANNER : false; +} + +function validateAppendObject(validationFunction, allowedKeys, inputObject, appendToObject) { + const outputObject = { + ...appendToObject + }; + if (allowedKeys.length > 0 && typeof validationFunction === 'function') { + for (const objectKey in inputObject) { + if (allowedKeys.indexOf(objectKey) !== -1 && validationFunction(inputObject[objectKey])) { + outputObject[objectKey] = inputObject[objectKey] + } + } + } + return outputObject; +}; + +function getTtl(bidderRequest) { + const ttl = config.getConfig('adtrgtme.ttl'); + const validateTTL = (ttl) => { + return (isNumber(ttl) && ttl > 0 && ttl < 3600) ? ttl : DEFAULT_BID_TTL + }; + return ttl ? validateTTL(ttl) : validateTTL(deepAccess(bidderRequest, 'params.ttl')); +}; + +function getFloorModuleData(bid) { + const getFloorRequestObject = { + currency: deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY, + mediaType: BANNER, + size: '*' + }; + return (isFn(bid.getFloor)) ? bid.getFloor(getFloorRequestObject) : false; +}; + +function generateOpenRtbObject(bidderRequest, bid) { + if (bidderRequest) { + let outBoundBidRequest = { + id: generateUUID(), + cur: [getFloorModuleData(bidderRequest).currency || deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY], + imp: [], + site: { + page: deepAccess(bidderRequest, 'refererInfo.page') + }, + device: { + dnt: 0, + ua: navigator.userAgent, + ip: deepAccess(bid, 'params.bidOverride.device.ip') || deepAccess(bid, 'params.ext.ip') || undefined + }, + regs: { + ext: { + 'us_privacy': bidderRequest.uspConsent ? bidderRequest.uspConsent : '', + gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } + }, + source: { + ext: { + hb: 1, + adapterver: ADAPTER_VERSION, + prebidver: PREBID_VERSION, + integration: { + name: INTEGRATION_METHOD, + ver: PREBID_VERSION + } + }, + fd: 1 + }, + user: { + ext: { + consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies + ? bidderRequest.gdprConsent.consentString : '' + } + } + }; + + outBoundBidRequest.site.id = bid.params.sid; + + if (bidderRequest.ortb2) { + outBoundBidRequest = appendFirstPartyData(outBoundBidRequest, bid); + }; + + if (deepAccess(bid, 'schain')) { + outBoundBidRequest.source.ext.schain = bid.schain; + outBoundBidRequest.source.ext.schain.nodes[0].rid = outBoundBidRequest.id; + }; + + return outBoundBidRequest; + }; +}; + +function appendImpObject(bid, openRtbObject) { + const mediaTypeMode = getMediaType(bid); + + if (openRtbObject && bid) { + const impObject = { + id: bid.bidId, + secure: isSecure(bid), + bidfloor: getFloorModuleData(bid).floor || deepAccess(bid, 'params.bidOverride.imp.bidfloor') || 0.000001 + }; + + if (mediaTypeMode === BANNER) { + impObject.banner = { + mimes: bid.mediaTypes.banner.mimes || ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: transformSizes(bid.sizes) + }; + if (bid.mediaTypes.banner.pos) { + impObject.banner.pos = bid.mediaTypes.banner.pos; + }; + }; + + impObject.ext = { + dfp_ad_unit_code: bid.adUnitCode + }; + + if (deepAccess(bid, 'params.zid')) { + impObject.tagid = bid.params.zid; + } + + if (deepAccess(bid, 'ortb2Imp.ext.data') && isPlainObject(bid.ortb2Imp.ext.data)) { + impObject.ext.data = bid.ortb2Imp.ext.data; + }; + + if (deepAccess(bid, 'ortb2Imp.instl') && isNumber(bid.ortb2Imp.instl) && (bid.ortb2Imp.instl === 1)) { + impObject.instl = bid.ortb2Imp.instl; + }; + + openRtbObject.imp.push(impObject); + }; +}; + +function appendFirstPartyData(outBoundBidRequest, bid) { + const ortb2Object = bid.ortb2; + const siteObject = deepAccess(ortb2Object, 'site') || undefined; + const siteContentObject = deepAccess(siteObject, 'content') || undefined; + const userObject = deepAccess(ortb2Object, 'user') || undefined; + + if (siteObject && isPlainObject(siteObject)) { + const allowedSiteStringKeys = ['name', 'domain', 'page', 'ref', 'keywords']; + const allowedSiteArrayKeys = ['cat', 'sectioncat', 'pagecat']; + const allowedSiteObjectKeys = ['ext']; + outBoundBidRequest.site = validateAppendObject(isStr, allowedSiteStringKeys, siteObject, outBoundBidRequest.site); + outBoundBidRequest.site = validateAppendObject(isArray, allowedSiteArrayKeys, siteObject, outBoundBidRequest.site); + outBoundBidRequest.site = validateAppendObject(isPlainObject, allowedSiteObjectKeys, siteObject, outBoundBidRequest.site); + }; + + if (siteContentObject && isPlainObject(siteContentObject)) { + const allowedContentStringKeys = ['id', 'title', 'language']; + const allowedContentArrayKeys = ['cat']; + outBoundBidRequest.site.content = validateAppendObject(isStr, allowedContentStringKeys, siteContentObject, outBoundBidRequest.site.content); + outBoundBidRequest.site.content = validateAppendObject(isArray, allowedContentArrayKeys, siteContentObject, outBoundBidRequest.site.content); + }; + + if (userObject && isPlainObject(userObject)) { + const allowedUserStrings = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; + const allowedUserObjects = ['ext']; + outBoundBidRequest.user = validateAppendObject(isStr, allowedUserStrings, userObject, outBoundBidRequest.user); + outBoundBidRequest.user.ext = validateAppendObject(isPlainObject, allowedUserObjects, userObject, outBoundBidRequest.user.ext); + }; + + return outBoundBidRequest; +}; + +function generateServerRequest({payload, requestOptions, bidderRequest}) { + return { + url: (config.getConfig('adtrgtme.endpoint') || ENDPOINT) + (payload.site.id || ''), + method: 'POST', + data: payload, + options: requestOptions, + bidderRequest: bidderRequest + }; +}; + +export const spec = { + code: BIDDER_CODE, + aliases: [], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + const params = bid.params; + if (isPlainObject(params) && isNumber(params.sid)) { + return true; + } else { + logWarn('Adtrgtme bidder params missing or incorrect'); + return false; + } + }, + + buildRequests: function(validBidRequests, bidderRequest) { + if (isEmpty(validBidRequests) || isEmpty(bidderRequest)) { + logWarn('Adtrgtme Adapter: buildRequests called with empty request'); + return undefined; + }; + + const requestOptions = { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.5' + } + }; + + requestOptions.withCredentials = hasPurpose1Consent(bidderRequest.gdprConsent); + + if (config.getConfig('adtrgtme.singleRequestMode') === true) { + const payload = generateOpenRtbObject(bidderRequest, validBidRequests[0]); + validBidRequests.forEach(bid => { + appendImpObject(bid, payload); + }); + + return generateServerRequest({payload, requestOptions, bidderRequest}); + } + + return validBidRequests.map(bid => { + const payloadClone = generateOpenRtbObject(bidderRequest, bid); + appendImpObject(bid, payloadClone); + + return generateServerRequest({payload: payloadClone, requestOptions, bidderRequest: bid}); + }); + }, + + interpretResponse: function(serverResponse, { data, bidderRequest }) { + const response = []; + if (!serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) { + return response; + } + + let seatbids = serverResponse.body.seatbid; + seatbids.forEach(seatbid => { + let bid; + + try { + bid = seatbid.bid[0]; + } catch (e) { + return response; + } + + let cpm = bid.price; + + let bidResponse = { + adId: deepAccess(bid, 'adId') ? bid.adId : bid.impid || bid.crid, + ad: bid.adm, + adUnitCode: bidderRequest.adUnitCode, + requestId: bid.impid, + cpm: cpm, + width: bid.w, + height: bid.h, + creativeId: bid.crid || 0, + currency: bid.cur || DEFAULT_CURRENCY, + dealId: bid.dealid ? bid.dealid : null, + netRevenue: true, + ttl: getTtl(bidderRequest), + mediaType: BANNER, + meta: { + advertiserDomains: bid.adomain, + mediaType: BANNER, + } + }; + + response.push(bidResponse); + }); + + return response; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const bidResponse = !isEmpty(serverResponses) && serverResponses[0].body; + if (bidResponse && bidResponse.ext && bidResponse.ext.pixels) { + return extractUserSyncUrls(syncOptions, bidResponse.ext.pixels); + } + return []; + } +}; + +registerBidder(spec); diff --git a/modules/adtrgtmeBidAdapter.md b/modules/adtrgtmeBidAdapter.md new file mode 100644 index 00000000000..d136b17067d --- /dev/null +++ b/modules/adtrgtmeBidAdapter.md @@ -0,0 +1,69 @@ +# Overview + +**Module Name**: adtrgtme Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: info@adtarget.me + +# Description +The Adtrgtme Bid Adapter is an OpenRTB interface that support display demand from Adtarget + +# Supported Features: +* Media Types: Banner +* Multi-format adUnits +* Price floors module +* Advertiser domains + +# Mandatory Bidder Parameters +The minimal requirements for the 'adtrgtme' bid adapter to generate an outbound bid-request to our Adtrgtme are: +1. At least 1 banner adUnit +2. Your Adtrgtme site id **bidder.params**.**sid** + +## Example: +```javascript +const adUnits = [{ + code: 'your-placement', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'adtrgtme', + params: { + sid: 1220291391, // Site/App ID provided from SSP + } + } + ] +}]; +``` + +# Optional: Price floors module & bidfloor +The adtargerme adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency. +By default the adapter will always check the existance of Module price floor. +If a module price floor does not exist you can set a custom bid floor for your impression using "params.bidOverride.imp.bidfloor". + +```javascript +const adUnits = [{ + code: 'your-placement', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [{ + bidder: 'adtrgtme', + params: { + sid: 1220291391, + bidOverride :{ + imp: { + bidfloor: 5.00 // bidOverride bidfloor + } + } + } + } + }] +}]; +``` \ No newline at end of file diff --git a/test/spec/modules/adtrgtmeBidAdapter_spec.js b/test/spec/modules/adtrgtmeBidAdapter_spec.js new file mode 100644 index 00000000000..fce270b4ea7 --- /dev/null +++ b/test/spec/modules/adtrgtmeBidAdapter_spec.js @@ -0,0 +1,802 @@ +import { expect } from 'chai'; +import { config } from 'src/config.js'; +import { BANNER } from 'src/mediaTypes.js'; +import { spec } from 'modules/adtrgtmeBidAdapter.js'; + +const DEFAULT_SID = 1220291391; +const DEFAULT_ZID = 1836455615; +const DEFAULT_BID_ID = '84ab500420319d'; + +const DEFAULT_AD_UNIT_CODE = '/1220291391/header-banner'; +const DEFAULT_AD_UNIT_TYPE = BANNER; +const DEFAULT_PARAMS_BID_OVERRIDE = {}; + +const ADAPTER_VERSION = '1.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const INTEGRATION_METHOD = 'prebid.js'; + +// Utility functions +const generateBidRequest = ({bidId, adUnitCode, bidOverrideObject, zid, ortb2}) => { + const bidRequest = { + adUnitCode, + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId, + bidderRequestsCount: 1, + bidder: 'adtrgtme', + bidderRequestId: '7101db09af0db2', + bidderWinsCount: 0, + mediaTypes: {}, + params: { + bidOverride: bidOverrideObject + }, + src: 'client', + transactionId: '5b17b67d-7704-4732-8cc9-5b1723e9bcf9', + ortb2 + }; + + const bannerObj = { + sizes: [[300, 250]] + }; + + bidRequest.mediaTypes.banner = bannerObj; + bidRequest.sizes = [[300, 250]]; + + bidRequest.params.sid = DEFAULT_SID; + if (typeof zid == 'number') { + bidRequest.params.zid = zid; + } + + return bidRequest; +} + +let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { + const bidderRequest = { + adUnitCode: adUnitCode || 'default-adUnitCode', + auctionId: 'd4c83a3b-18e4-4208-b98b-63848449c7aa', + auctionStart: new Date().getTime(), + bidderCode: 'adtrgtme', + bidderRequestId: '112f1c7c5d399a', + bids: bidRequestArray, + refererInfo: { + page: 'https://publisher-test.com', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['https://publisher-test.com'], + }, + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + vendorData: {}, + gdprApplies: true + }, + start: new Date().getTime(), + timeout: 1000, + ortb2 + }; + + return bidderRequest; +}; + +const generateBuildRequestMock = ({bidId, adUnitCode, adUnitType, zid, bidOverrideObject, pubIdMode, ortb2}) => { + const bidRequestConfig = { + bidId: bidId || DEFAULT_BID_ID, + adUnitCode: adUnitCode || DEFAULT_AD_UNIT_CODE, + adUnitType: adUnitType || DEFAULT_AD_UNIT_TYPE, + zid: zid || DEFAULT_ZID, + bidOverrideObject: bidOverrideObject || DEFAULT_PARAMS_BID_OVERRIDE, + + pubIdMode: pubIdMode || false, + ortb2: ortb2 || {} + }; + const bidRequest = generateBidRequest(bidRequestConfig); + const validBidRequests = [bidRequest]; + const bidderRequest = generateBidderRequest(validBidRequests, adUnitCode, ortb2); + + return { bidRequest, validBidRequests, bidderRequest } +}; + +const generateAdmPayload = (admPayloadType) => { + let ADM_PAYLOAD; + switch (admPayloadType) { + case 'banner': + ADM_PAYLOAD = ''; // banner + break; + default: ''; break; + }; + + return ADM_PAYLOAD; +}; + +const generateResponseMock = (admPayloadType) => { + const bidResponse = { + id: 'fc0c35df-21fb-4f93-9ebd-88759dbe31f9', + impid: '274395c06a24e5', + adm: generateAdmPayload(admPayloadType), + price: 1, + w: 300, + h: 250, + crid: 'ssp-placement-name', + adomain: ['advertiser-domain.com'] + }; + + const serverResponse = { + body: { + id: 'fc0c35df-21fb-4f93-9ebd-88759dbe31f9', + seatbid: [{ bid: [ bidResponse ], seat: 13107 }] + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + + return {serverResponse, data, bidderRequest}; +} + +// Unit tests +describe('adtrgtme Bid Adapter:', () => { + it('PLACEHOLDER TO PASS GULP', () => { + const obj = {}; + expect(obj).to.be.an('object'); + }); + + describe('Validate basic properties', () => { + it('should define the correct bidder code', () => { + expect(spec.code).to.equal('adtrgtme') + }); + }); + + describe('getUserSyncs()', () => { + const IMAGE_PIXEL_URL = 'http://image-pixel.com/foo/bar?1234&baz=true'; + const IFRAME_ONE_URL = 'http://image-iframe.com/foo/bar?1234&baz=true'; + const IFRAME_TWO_URL = 'http://image-iframe-two.com/foo/bar?1234&baz=true'; + + let serverResponses = []; + beforeEach(() => { + serverResponses[0] = { + body: { + ext: { + pixels: `` + } + } + } + }); + + after(() => { + serverResponses = undefined; + }); + + it('for only iframe enabled syncs', () => { + let syncOptions = { + iframeEnabled: true, + pixelEnabled: false + }; + let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); + expect(pixelsObjects.length).to.equal(2); + expect(pixelsObjects).to.deep.equal( + [ + {type: 'iframe', 'url': IFRAME_ONE_URL}, + {type: 'iframe', 'url': IFRAME_TWO_URL} + ] + ) + }); + + it('for only pixel enabled syncs', () => { + let syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); + expect(pixelsObjects.length).to.equal(1); + expect(pixelsObjects).to.deep.equal( + [ + {type: 'image', 'url': IMAGE_PIXEL_URL} + ] + ) + }); + + it('for both pixel and iframe enabled syncs', () => { + let syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); + expect(pixelsObjects.length).to.equal(3); + expect(pixelsObjects).to.deep.equal( + [ + {type: 'iframe', 'url': IFRAME_ONE_URL}, + {type: 'image', 'url': IMAGE_PIXEL_URL}, + {type: 'iframe', 'url': IFRAME_TWO_URL} + ] + ) + }); + }); + + // Validate Bid Requests + describe('isBidRequestValid()', () => { + const INVALID_INPUT = [ + {}, + {params: {}}, + {params: {sid: '1234', zid: '4321'}}, + {params: {sid: '1220291391', zid: 4321}}, + {params: {zid: ''}}, + {params: {sid: '', zid: ''}}, + ]; + + INVALID_INPUT.forEach(input => { + it(`should determine that the bid is INVALID for the input ${JSON.stringify(input)}`, () => { + expect(spec.isBidRequestValid(input)).to.be.false; + }); + }); + + it('should determine that the bid is VALID if sid and zid are present on the params object', () => { + const validBid = { + params: { + sid: 1220291391, + zid: 1836455615 + } + }; + expect(spec.isBidRequestValid(validBid)).to.be.true; + }); + }); + + describe('Price Floor module support:', () => { + it('should get bidfloor from getFloor method', () => { + const { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); + bidRequest.params.bidOverride = {cur: 'EUR'}; + bidRequest.getFloor = (floorObj) => { + return { + floor: bidRequest.floors.values[floorObj.mediaType + '|300x250'], + currency: floorObj.currency, + mediaType: floorObj.mediaType + } + }; + bidRequest.floors = { + currency: 'EUR', + values: { + 'banner|300x250': 5.55 + } + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.cur).to.deep.equal(['EUR']); + expect(data.imp[0].bidfloor).is.a('number'); + expect(data.imp[0].bidfloor).to.equal(5.55); + }); + }); + + describe('Schain module support:', () => { + it('should send Global or Bidder specific schain', function () { + const { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const globalSchain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'some-platform.com', + sid: '111111', + rid: bidRequest.bidId, + hp: 1 + }] + }; + bidRequest.schain = globalSchain; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const schain = data.source.ext.schain; + expect(schain.nodes.length).to.equal(1); + expect(schain).to.equal(globalSchain); + }); + }); + + describe('First party data module - "Site" support (ortb2):', () => { + // Should not allow invalid "site" data types + const INVALID_ORTB2_TYPES = [ null, [], 123, 'unsupportedKeyName', true, false, undefined ]; + INVALID_ORTB2_TYPES.forEach(param => { + it(`should not allow invalid site types to be added to bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { site: param } + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site[param]).to.be.undefined; + }); + }); + + // Should add valid "site" params + const VALID_SITE_STRINGS = ['name', 'domain', 'page', 'ref', 'keywords']; + const VALID_SITE_ARRAYS = ['cat', 'sectioncat', 'pagecat']; + + VALID_SITE_STRINGS.forEach(param => { + it(`should allow supported site keys to be added bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + site: { + [param]: 'something' + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site[param]).to.exist; + expect(data.site[param]).to.be.a('string'); + expect(data.site[param]).to.be.equal(ortb2.site[param]); + }); + }); + + VALID_SITE_ARRAYS.forEach(param => { + it(`should determine that the ortb2.site Array key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + site: { + [param]: ['something'] + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site[param]).to.exist; + expect(data.site[param]).to.be.a('array'); + expect(data.site[param]).to.be.equal(ortb2.site[param]); + }); + }); + + // Should not allow invalid "site.content" data types + INVALID_ORTB2_TYPES.forEach(param => { + it(`should determine that the ortb2.site.content key is invalid and should not be added to bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + site: { + content: param + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site.content).to.be.undefined; + }); + }); + + // Should not allow invalid "site.content" keys + it(`should not allow invalid ortb2.site.content object keys to be added to bid-request: {custom object}`, () => { + const ortb2 = { + site: { + content: { + fake: 'news', + unreal: 'param', + counterfit: 'data' + } + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site.content).to.be.a('object'); + }); + + // Should append valid "site.content" keys + const VALID_CONTENT_STRINGS = ['id', 'title', 'language']; + VALID_CONTENT_STRINGS.forEach(param => { + it(`should determine that the ortb2.site String key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + site: { + content: { + [param]: 'something' + } + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site.content[param]).to.exist; + expect(data.site.content[param]).to.be.a('string'); + expect(data.site.content[param]).to.be.equal(ortb2.site.content[param]); + }); + }); + + const VALID_CONTENT_ARRAYS = ['cat']; + VALID_CONTENT_ARRAYS.forEach(param => { + it(`should determine that the ortb2.site Array key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + site: { + content: { + [param]: ['something', 'something-else'] + } + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.site.content[param]).to.be.a('array'); + expect(data.site.content[param]).to.be.equal(ortb2.site.content[param]); + }); + }); + }); + + describe('First party data module - "User" support (ortb2):', () => { + // Global ortb2.user validations + // Should not allow invalid "user" data types + const INVALID_ORTB2_TYPES = [ null, [], 'unsupportedKeyName', true, false, undefined ]; + INVALID_ORTB2_TYPES.forEach(param => { + it(`should not allow invalid site types to be added to bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { user: param } + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.user[param]).to.be.undefined; + }); + }); + + // Should add valid "user" params + const VALID_USER_STRINGS = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; + VALID_USER_STRINGS.forEach(param => { + it(`should allow supported user string keys to be added bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + user: { + [param]: 'something' + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.user[param]).to.exist; + expect(data.user[param]).to.be.a('string'); + expect(data.user[param]).to.be.equal(ortb2.user[param]); + }); + }); + + const VALID_USER_OBJECTS = ['ext']; + VALID_USER_OBJECTS.forEach(param => { + it(`should allow supported user extObject keys to be added to the bid-request: ${JSON.stringify(param)}`, () => { + const ortb2 = { + user: { + [param]: {a: '123', b: '456'} + } + }; + const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.user[param]).to.exist; + expect(data.user[param]).to.be.a('object'); + expect(data.user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); + config.setConfig({ortb2: {}}); + }); + }); + + // adUnit.ortb2Imp.ext.data + it(`should allow adUnit.ortb2Imp.ext.data object to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + } + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].ext.data).to.deep.equal(validBidRequests[0].ortb2Imp.ext.data); + }); + // adUnit.ortb2Imp.instl + it(`should allow adUnit.ortb2Imp.instl numeric boolean "1" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: 1 + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.deep.equal(validBidRequests[0].ortb2Imp.instl); + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "true" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: true + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "false" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: false + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); + }); + + describe('GDPR & Consent:', () => { + it('should return request objects that do not send cookies if purpose 1 consent is not provided', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + bidderRequest.gdprConsent = { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + apiVersion: 2, + vendorData: { + purpose: { + consents: { + '1': false + } + } + }, + gdprApplies: true + }; + const options = spec.buildRequests(validBidRequests, bidderRequest)[0].options; + expect(options.withCredentials).to.be.false; + }); + }); + + describe('Endpoint & Impression Request Mode:', () => { + it('should route request to config override endpoint', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const sid = validBidRequests[0].params.sid; + const testOverrideEndpoint = 'http://new_bidder_host.com/ssp?s='; + config.setConfig({ + adtrgtme: { + endpoint: testOverrideEndpoint + } + }); + const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; + expect(response).to.deep.include( + { + method: 'POST', + url: testOverrideEndpoint + sid + }); + }); + + it('should route request to endpoint + sid', () => { + config.setConfig({ + adtrgtme: {} + }); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const sid = validBidRequests[0].params.sid; + const response = spec.buildRequests(validBidRequests, bidderRequest); + expect(response[0]).to.deep.include({ + method: 'POST', + url: 'https://z.cdn.adtarget.market/ssp?prebid&s=' + sid + }); + }); + + it('should return a single request object for single request mode', () => { + let { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const BID_ID_2 = '84ab50xxxxx'; + const BID_ZID_2 = 98876543210; + const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; + const { bidRequest: bidRequest2 } = generateBuildRequestMock({bidId: BID_ID_2, zid: BID_ZID_2, adUnitCode: AD_UNIT_CODE_2}); + validBidRequests = [bidRequest, bidRequest2]; + bidderRequest.bids = validBidRequests; + + config.setConfig({ + adtrgtme: { + singleRequestMode: true + } + }); + + const data = spec.buildRequests(validBidRequests, bidderRequest).data; + + expect(data.imp).to.be.an('array').with.lengthOf(2); + + expect(data.imp[0]).to.deep.include({ + id: DEFAULT_BID_ID, + ext: { + dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE + } + }); + + expect(data.imp[1]).to.deep.include({ + id: BID_ID_2, + tagid: BID_ZID_2, + ext: { + dfp_ad_unit_code: AD_UNIT_CODE_2 + } + }); + }); + }); + + describe('Validate request filtering:', () => { + it('should not return request when no bids are present', function () { + let request = spec.buildRequests([]); + expect(request).to.be.undefined; + }); + + it('buildRequests(): should return an array with the correct amount of request objects', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const response = spec.buildRequests(validBidRequests, bidderRequest).bidderRequest; + expect(response.bids).to.be.an('array').to.have.lengthOf(1); + }); + }); + + describe('Request Headers validation:', () => { + it('should return request objects with the relevant custom headers and content type declaration', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + bidderRequest.gdprConsent.gdprApplies = false; + const options = spec.buildRequests(validBidRequests, bidderRequest).options; + expect(options).to.deep.equal( + { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.5' + }, + withCredentials: true + }); + }); + }); + + describe('Request Payload oRTB bid validation:', () => { + it('should generate a valid openRTB bid-request object in the data field', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const data = spec.buildRequests(validBidRequests, bidderRequest).data; + expect(data.site).to.deep.include({ + id: bidderRequest.bids[0].params.sid, + page: bidderRequest.refererInfo.page + }); + + expect(data.device).to.deep.equal({ + dnt: 0, + ua: navigator.userAgent, + ip: undefined + }); + + expect(data.regs).to.deep.equal({ + ext: { + 'us_privacy': '', + gdpr: 1 + } + }); + + expect(data.source).to.deep.equal({ + ext: { + hb: 1, + adapterver: ADAPTER_VERSION, + prebidver: PREBID_VERSION, + integration: { + name: INTEGRATION_METHOD, + ver: PREBID_VERSION + } + }, + fd: 1 + }); + + expect(data.cur).to.deep.equal(['USD']); + }); + + it('should generate a valid openRTB imp.ext object in the bid-request', () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const bid = validBidRequests[0]; + const data = spec.buildRequests(validBidRequests, bidderRequest).data; + expect(data.imp[0].ext).to.deep.equal({ + dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE + }); + }); + + it('should use siteId value as site.id in the outbound bid-request when using "pubId" integration mode', () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); + validBidRequests[0].params.sid = 9876543210; + const data = spec.buildRequests(validBidRequests, bidderRequest).data; + expect(data.site.id).to.equal(9876543210); + }); + + it('should use placementId value as imp.tagid in the outbound bid-request when using "zid"', () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}), + TEST_ZID = 54321; + validBidRequests[0].params.zid = TEST_ZID; + const data = spec.buildRequests(validBidRequests, bidderRequest).data; + expect(data.imp[0].tagid).to.deep.equal(TEST_ZID); + }); + }); + + describe('Request Payload oRTB bid.imp validation:', () => { + // Validate Banner imp imp when adtrgtme.mode=undefined + it('should generate a valid "Banner" imp object', () => { + config.setConfig({ + adtrgtme: {} + }); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}] + }); + }); + + // Validate Banner imp + it('should generate a valid "Banner" imp object', () => { + config.setConfig({ + adtrgtme: { mode: 'banner' } + }); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}] + }); + }); + }); + + describe('interpretResponse()', () => { + describe('for mediaTypes: "banner"', () => { + it('should insert banner payload into response[0].ad', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ad).to.equal(''); + expect(response[0].mediaType).to.equal('banner'); + }) + }); + + describe('Support Advertiser domains', () => { + it('should append bid-response adomain to meta.advertiserDomains', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].meta.advertiserDomains).to.be.a('array'); + expect(response[0].meta.advertiserDomains[0]).to.equal('advertiser-domain.com'); + }) + }); + + describe('bid response Ad ID / Creative ID', () => { + it('should use adId if it exists in the bid-response', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const adId = 'bid-response-adId'; + serverResponse.body.seatbid[0].bid[0].adId = adId; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].adId).to.equal(adId); + }); + + it('should use impid if adId does not exist in the bid-response', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const impid = '25b6c429c1f52f'; + serverResponse.body.seatbid[0].bid[0].impid = impid; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].adId).to.equal(impid); + }); + + it('should use crid if adId & impid do not exist in the bid-response', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const crid = 'passback-12579'; + serverResponse.body.seatbid[0].bid[0].impid = undefined; + serverResponse.body.seatbid[0].bid[0].crid = crid; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].adId).to.equal(crid); + }); + }); + + describe('Time To Live (ttl)', () => { + const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; + UNSUPPORTED_TTL_FORMATS.forEach(param => { + it('should not allow unsupported global adtrgtme.ttl formats and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + config.setConfig({ + adtrgtme: { ttl: param } + }); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + + it('should not allow unsupported params.ttl formats and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + }); + + const UNSUPPORTED_TTL_VALUES = [-1, 3601]; + UNSUPPORTED_TTL_VALUES.forEach(param => { + it('should not allow invalid global adtrgtme.ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + config.setConfig({ + adtrgtme: { ttl: param } + }); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + + it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + }); + + it('should give presedence to Gloabl ttl over params.ttl ', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + config.setConfig({ + adtrgtme: { ttl: 500 } + }); + bidderRequest.bids[0].params.ttl = 400; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(500); + }); + }); + + describe('Aliasing support', () => { + it('should return undefined as the bidder code value', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].bidderCode).to.be.undefined; + }); + }); + }); +});