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;
+ });
+ });
+ });
+});