diff --git a/modules/1ad4goodBidAdapter.js b/modules/1ad4goodBidAdapter.js
new file mode 100644
index 00000000000..26057b5e2b2
--- /dev/null
+++ b/modules/1ad4goodBidAdapter.js
@@ -0,0 +1,399 @@
+import * as utils from '../src/utils';
+import { registerBidder } from '../src/adapters/bidderFactory';
+import { BANNER, VIDEO } from '../src/mediaTypes';
+import find from 'core-js/library/fn/array/find';
+import includes from 'core-js/library/fn/array/includes';
+
+const BIDDER_CODE = '1ad4good';
+const URL = 'https://hb.1ad4good.org/prebid';
+const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration',
+ 'startdelay', 'skippable', 'playback_method', 'frameworks'];
+const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language'];
+const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately
+const SOURCE = 'pbjs';
+const MAX_IMPS_PER_REQUEST = 15;
+
+export const spec = {
+ code: BIDDER_CODE,
+ aliases: ['adsforgood', 'ads4good', '1adsforgood'],
+ supportedMediaTypes: [BANNER, VIDEO],
+
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {object} bid The bid to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function(bid) {
+ return !!(bid.params.placementId);
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
+ * @return ServerRequest Info describing the request to the server.
+ */
+ buildRequests: function(bidRequests, bidderRequest) {
+ const tags = bidRequests.map(bidToTag);
+ const userObjBid = find(bidRequests, hasUserInfo);
+ let userObj;
+ if (userObjBid) {
+ userObj = {};
+ Object.keys(userObjBid.params.user)
+ .filter(param => includes(USER_PARAMS, param))
+ .forEach(param => userObj[param] = userObjBid.params.user[param]);
+ }
+
+ const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo);
+ let appDeviceObj;
+ if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) {
+ appDeviceObj = {};
+ Object.keys(appDeviceObjBid.params.app)
+ .filter(param => includes(APP_DEVICE_PARAMS, param))
+ .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]);
+ }
+
+ const appIdObjBid = find(bidRequests, hasAppId);
+ let appIdObj;
+ if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) {
+ appIdObj = {
+ appid: appIdObjBid.params.app.id
+ };
+ }
+
+ const payload = {
+ tags: [...tags],
+ user: userObj,
+ sdk: {
+ source: SOURCE,
+ version: '$prebid.version$'
+ }
+ };
+
+ if (appDeviceObjBid) {
+ payload.device = appDeviceObj
+ }
+ if (appIdObjBid) {
+ payload.app = appIdObj;
+ }
+
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ // note - objects for impbus use underscore instead of camelCase
+ payload.gdpr_consent = {
+ consent_string: bidderRequest.gdprConsent.consentString,
+ consent_required: bidderRequest.gdprConsent.gdprApplies
+ };
+ }
+
+ if (bidderRequest && bidderRequest.refererInfo) {
+ let refererinfo = {
+ rd_ref: encodeURIComponent(bidderRequest.refererInfo.referer),
+ rd_top: bidderRequest.refererInfo.reachedTop,
+ rd_ifs: bidderRequest.refererInfo.numIframes,
+ rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',')
+ }
+ payload.referrer_detection = refererinfo;
+ }
+
+ const request = formatRequest(payload, bidderRequest);
+ return request;
+ },
+
+ /**
+ * Unpack the response from the server into a list of bids.
+ *
+ * @param {*} serverResponse A successful response from the server.
+ * @return {Bid[]} An array of bids which were nested inside the server.
+ */
+ interpretResponse: function(serverResponse, {bidderRequest}) {
+ serverResponse = serverResponse.body;
+ const bids = [];
+ if (!serverResponse || serverResponse.error) {
+ let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`;
+ if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; }
+ utils.logError(errorMessage);
+ return bids;
+ }
+
+ if (serverResponse.tags) {
+ serverResponse.tags.forEach(serverBid => {
+ const rtbBid = getRtbBid(serverBid);
+ if (rtbBid) {
+ if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) {
+ const bid = newBid(serverBid, rtbBid, bidderRequest);
+ bid.mediaType = parseMediaType(rtbBid);
+ bids.push(bid);
+ }
+ }
+ });
+ }
+
+ return bids;
+ },
+
+ transformBidParams: function(params, isOpenRtb) {
+ params = utils.convertTypes({
+ 'placementId': 'number',
+ 'keywords': utils.transformBidderParamKeywords
+ }, params);
+
+ if (isOpenRtb) {
+ params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false;
+ if (params.usePaymentRule) { delete params.usePaymentRule; }
+
+ if (isPopulatedArray(params.keywords)) {
+ params.keywords.forEach(deleteValues);
+ }
+
+ Object.keys(params).forEach(paramKey => {
+ let convertedKey = utils.convertCamelToUnderscore(paramKey);
+ if (convertedKey !== paramKey) {
+ params[convertedKey] = params[paramKey];
+ delete params[paramKey];
+ }
+ });
+ }
+
+ return params;
+ },
+
+ /**
+ * Add element selector to javascript tracker to improve native viewability
+ * @param {Bid} bid
+ */
+ onBidWon: function(bid) {
+ }
+}
+
+function isPopulatedArray(arr) {
+ return !!(utils.isArray(arr) && arr.length > 0);
+}
+
+function deleteValues(keyPairObj) {
+ if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') {
+ delete keyPairObj.value;
+ }
+}
+
+function formatRequest(payload, bidderRequest) {
+ let request = [];
+
+ if (payload.tags.length > MAX_IMPS_PER_REQUEST) {
+ const clonedPayload = utils.deepClone(payload);
+
+ utils.chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => {
+ clonedPayload.tags = tags;
+ const payloadString = JSON.stringify(clonedPayload);
+ request.push({
+ method: 'POST',
+ url: URL,
+ data: payloadString,
+ bidderRequest
+ });
+ });
+ } else {
+ const payloadString = JSON.stringify(payload);
+ request = {
+ method: 'POST',
+ url: URL,
+ data: payloadString,
+ bidderRequest
+ };
+ }
+
+ return request;
+}
+
+/**
+ * Unpack the Server's Bid into a Prebid-compatible one.
+ * @param serverBid
+ * @param rtbBid
+ * @param bidderRequest
+ * @return Bid
+ */
+function newBid(serverBid, rtbBid, bidderRequest) {
+ const bidRequest = utils.getBidRequest(serverBid.uuid, [bidderRequest]);
+ const bid = {
+ requestId: serverBid.uuid,
+ cpm: rtbBid.cpm,
+ creativeId: rtbBid.creative_id,
+ dealId: rtbBid.deal_id,
+ currency: 'USD',
+ netRevenue: true,
+ ttl: 300,
+ adUnitCode: bidRequest.adUnitCode,
+ ads4good: {
+ buyerMemberId: rtbBid.buyer_member_id,
+ dealPriority: rtbBid.deal_priority,
+ dealCode: rtbBid.deal_code
+ }
+ };
+
+ if (rtbBid.advertiser_id) {
+ bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id });
+ }
+
+ if (rtbBid.rtb.video) {
+ Object.assign(bid, {
+ width: rtbBid.rtb.video.player_width,
+ height: rtbBid.rtb.video.player_height,
+ vastUrl: rtbBid.rtb.video.asset_url,
+ vastImpUrl: rtbBid.notify_url,
+ ttl: 3600
+ });
+ } else {
+ Object.assign(bid, {
+ width: rtbBid.rtb.banner.width,
+ height: rtbBid.rtb.banner.height,
+ ad: rtbBid.rtb.banner.content
+ });
+ try {
+ const url = rtbBid.rtb.trackers[0].impression_urls[0];
+ const tracker = utils.createTrackPixelHtml(url);
+ bid.ad += tracker;
+ } catch (error) {
+ utils.logError('Error appending tracking pixel', error);
+ }
+ }
+
+ return bid;
+}
+
+function bidToTag(bid) {
+ const tag = {};
+ tag.sizes = transformSizes(bid.sizes);
+ tag.primary_size = tag.sizes[0];
+ tag.ad_types = [];
+ tag.uuid = bid.bidId;
+ if (bid.params.placementId) {
+ tag.id = parseInt(bid.params.placementId, 10);
+ }
+ if (bid.params.cpm) {
+ tag.cpm = bid.params.cpm;
+ }
+ tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false;
+ tag.use_pmt_rule = bid.params.usePaymentRule || false
+ tag.prebid = true;
+ tag.disable_psa = true;
+ if (bid.params.reserve) {
+ tag.reserve = bid.params.reserve;
+ }
+ if (bid.params.position) {
+ tag.position = {'above': 1, 'below': 2}[bid.params.position] || 0;
+ }
+ if (bid.params.trafficSourceCode) {
+ tag.traffic_source_code = bid.params.trafficSourceCode;
+ }
+ if (bid.params.privateSizes) {
+ tag.private_sizes = transformSizes(bid.params.privateSizes);
+ }
+ if (bid.params.supplyType) {
+ tag.supply_type = bid.params.supplyType;
+ }
+ if (bid.params.pubClick) {
+ tag.pubclick = bid.params.pubClick;
+ }
+ if (bid.params.extInvCode) {
+ tag.ext_inv_code = bid.params.extInvCode;
+ }
+ if (bid.params.externalImpId) {
+ tag.external_imp_id = bid.params.externalImpId;
+ }
+ if (!utils.isEmpty(bid.params.keywords)) {
+ let keywords = utils.transformBidderParamKeywords(bid.params.keywords);
+
+ if (keywords.length > 0) {
+ keywords.forEach(deleteValues);
+ }
+ tag.keywords = keywords;
+ }
+
+ const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`);
+ const context = utils.deepAccess(bid, 'mediaTypes.video.context');
+
+ if (bid.mediaType === VIDEO || videoMediaType) {
+ tag.ad_types.push(VIDEO);
+ }
+
+ // instream gets vastUrl, outstream gets vastXml
+ if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) {
+ tag.require_asset_url = true;
+ }
+
+ if (bid.params.video) {
+ tag.video = {};
+ // place any valid video params on the tag
+ Object.keys(bid.params.video)
+ .filter(param => includes(VIDEO_TARGETING, param))
+ .forEach(param => tag.video[param] = bid.params.video[param]);
+ }
+
+ if (bid.renderer) {
+ tag.video = Object.assign({}, tag.video, {custom_renderer_present: true});
+ }
+
+ if (
+ (utils.isEmpty(bid.mediaType) && utils.isEmpty(bid.mediaTypes)) ||
+ (bid.mediaType === BANNER || (bid.mediaTypes && bid.mediaTypes[BANNER]))
+ ) {
+ tag.ad_types.push(BANNER);
+ }
+
+ return tag;
+}
+
+/* Turn bid request sizes into ut-compatible format */
+function transformSizes(requestSizes) {
+ let sizes = [];
+ let sizeObj = {};
+
+ if (utils.isArray(requestSizes) && requestSizes.length === 2 &&
+ !utils.isArray(requestSizes[0])) {
+ sizeObj.width = parseInt(requestSizes[0], 10);
+ sizeObj.height = parseInt(requestSizes[1], 10);
+ sizes.push(sizeObj);
+ } else if (typeof requestSizes === 'object') {
+ for (let i = 0; i < requestSizes.length; i++) {
+ let size = requestSizes[i];
+ sizeObj = {};
+ sizeObj.width = parseInt(size[0], 10);
+ sizeObj.height = parseInt(size[1], 10);
+ sizes.push(sizeObj);
+ }
+ }
+
+ return sizes;
+}
+
+function hasUserInfo(bid) {
+ return !!bid.params.user;
+}
+
+function hasAppDeviceInfo(bid) {
+ if (bid.params) {
+ return !!bid.params.app
+ }
+}
+
+function hasAppId(bid) {
+ if (bid.params && bid.params.app) {
+ return !!bid.params.app.id
+ }
+ return !!bid.params.app
+}
+
+function getRtbBid(tag) {
+ return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb);
+}
+
+function parseMediaType(rtbBid) {
+ const adType = rtbBid.ad_type;
+ if (adType === VIDEO) {
+ return VIDEO;
+ } else {
+ return BANNER;
+ }
+}
+
+registerBidder(spec);
diff --git a/modules/1ad4goodBidAdapter.md b/modules/1ad4goodBidAdapter.md
new file mode 100644
index 00000000000..1e9dfe5e04c
--- /dev/null
+++ b/modules/1ad4goodBidAdapter.md
@@ -0,0 +1,87 @@
+# Overview
+
+```
+Module Name: 1ad4good Bid Adapter
+Module Type: Bidder Adapter
+Maintainer: info@1ad4good.org
+```
+
+# Description
+
+Connects to 1ad4good exchange for bids.
+
+1ad4good bid adapter supports Banner and Video (instream).
+
+# Test Parameters
+```
+var adUnits = [
+ // Banner adUnit
+ {
+ code: 'banner-div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]]
+ }
+ },
+ bids: [{
+ bidder: '1ad4good',
+ params: {
+ placementId: 13144370
+ }
+ }]
+ },
+ // Video instream adUnit
+ {
+ code: 'video-instream',
+ sizes: [[640, 480]],
+ mediaTypes: {
+ video: {
+ playerSize: [[640, 480]],
+ context: 'instream'
+ },
+ },
+ bids: [{
+ bidder: '1ad4good',
+ params: {
+ placementId: 13232361,
+ video: {
+ skippable: true,
+ playback_methods: ['auto_play_sound_off']
+ }
+ }
+ }]
+ },
+
+ // Banner adUnit in a App Webview
+ // Only use this for situations where prebid.js is in a webview of an App
+ // See Prebid Mobile for displaying ads via an SDK
+ {
+ code: 'banner-div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]]
+ }
+ }
+ bids: [{
+ bidder: '1ad4good',
+ params: {
+ placementId: 13144370,
+ app: {
+ id: "B1O2W3M4AN.com.prebid.webview",
+ geo: {
+ lat: 40.0964439,
+ lng: -75.3009142
+ },
+ device_id: {
+ idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier
+ aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier
+ md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID
+ sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID
+ windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier
+ }
+ }
+ }
+ }]
+ }
+];
+```
diff --git a/test/spec/modules/1ad4goodBidAdapter_spec.js b/test/spec/modules/1ad4goodBidAdapter_spec.js
new file mode 100644
index 00000000000..316a5c13233
--- /dev/null
+++ b/test/spec/modules/1ad4goodBidAdapter_spec.js
@@ -0,0 +1,548 @@
+import { expect } from 'chai';
+import { spec } from 'modules/1ad4goodBidAdapter';
+import { newBidder } from 'src/adapters/bidderFactory';
+import * as bidderFactory from 'src/adapters/bidderFactory';
+import { deepClone } from 'src/utils';
+import { config } from 'src/config';
+
+const ENDPOINT = 'https://hb.1ad4good.org/prebid';
+
+describe('AdforgoodAdapter', function () {
+ const adapter = newBidder(spec);
+
+ describe('inherited functions', function () {
+ it('exists and is a function', function () {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', function () {
+ let bid = {
+ 'bidder': '1ad4good',
+ 'params': {
+ 'placementId': '10433394'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ };
+
+ it('should return true when required params found', function () {
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+
+ it('should return false when required params are not passed', function () {
+ let bid = Object.assign({}, bid);
+ delete bid.params;
+ bid.params = {
+ 'placementId': 0
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', function () {
+ let bidRequests = [
+ {
+ 'bidder': '1ad4good',
+ 'params': {
+ 'placementId': '10433394'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ }
+ ];
+
+ it('should parse out private sizes', function () {
+ let bidRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ privateSizes: [300, 250]
+ }
+ }
+ );
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.tags[0].private_sizes).to.exist;
+ expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]);
+ });
+
+ it('should add source and verison to the tag', function () {
+ const request = spec.buildRequests(bidRequests);
+ const payload = JSON.parse(request.data);
+ expect(payload.sdk).to.exist;
+ expect(payload.sdk).to.deep.equal({
+ source: 'pbjs',
+ version: '$prebid.version$'
+ });
+ });
+
+ it('should populate the ad_types array on all requests', function () {
+ ['banner', 'video'].forEach(type => {
+ const bidRequest = Object.assign({}, bidRequests[0]);
+ bidRequest.mediaTypes = {};
+ bidRequest.mediaTypes[type] = {};
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.tags[0].ad_types).to.deep.equal([type]);
+ });
+ });
+
+ it('should populate the ad_types array on outstream requests', function () {
+ const bidRequest = Object.assign({}, bidRequests[0]);
+ bidRequest.mediaTypes = {};
+ bidRequest.mediaTypes.video = {context: 'outstream'};
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.tags[0].ad_types).to.deep.equal(['video']);
+ });
+
+ it('sends bid request to ENDPOINT via POST', function () {
+ const request = spec.buildRequests(bidRequests);
+ expect(request.url).to.equal(ENDPOINT);
+ expect(request.method).to.equal('POST');
+ });
+
+ it('should attach valid video params to the tag', function () {
+ let bidRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ video: {
+ id: 123,
+ minduration: 100,
+ foobar: 'invalid'
+ }
+ }
+ }
+ );
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+ expect(payload.tags[0].video).to.deep.equal({
+ id: 123,
+ minduration: 100
+ });
+ });
+
+ it('should add video property when adUnit includes a renderer', function () {
+ const videoData = {
+ mediaTypes: {
+ video: {
+ context: 'outstream',
+ mimes: ['video/mp4']
+ }
+ },
+ params: {
+ placementId: '10433394',
+ video: {
+ skippable: true,
+ playback_method: ['auto_play_sound_off']
+ }
+ }
+ };
+
+ let bidRequest1 = deepClone(bidRequests[0]);
+ bidRequest1 = Object.assign({}, bidRequest1, videoData, {
+ renderer: {
+ url: 'http://test.renderer.url',
+ render: function () {}
+ }
+ });
+
+ let bidRequest2 = deepClone(bidRequests[0]);
+ bidRequest2.adUnitCode = 'adUnit_code_2';
+ bidRequest2 = Object.assign({}, bidRequest2, videoData);
+
+ const request = spec.buildRequests([bidRequest1, bidRequest2]);
+ const payload = JSON.parse(request.data);
+ expect(payload.tags[0].video).to.deep.equal({
+ skippable: true,
+ playback_method: ['auto_play_sound_off'],
+ custom_renderer_present: true
+ });
+ expect(payload.tags[1].video).to.deep.equal({
+ skippable: true,
+ playback_method: ['auto_play_sound_off']
+ });
+ });
+
+ it('should attach valid user params to the tag', function () {
+ let bidRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ user: {
+ externalUid: '123',
+ foobar: 'invalid'
+ }
+ }
+ }
+ );
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.user).to.exist;
+ expect(payload.user).to.deep.equal({
+ externalUid: '123',
+ });
+ });
+
+ // it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () {
+ // let bidRequest = Object.assign({},
+ // bidRequests[0],
+ // {
+ // mediaType: 'native',
+ // nativeParams: {
+ // image: { required: true }
+ // }
+ // }
+ // );
+ // bidRequest.sizes = [[150, 100], [300, 250]];
+
+ // let request = spec.buildRequests([bidRequest]);
+ // let payload = JSON.parse(request.data);
+ // expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]);
+
+ // delete bidRequest.sizes;
+
+ // request = spec.buildRequests([bidRequest]);
+ // payload = JSON.parse(request.data);
+
+ // expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]);
+ // });
+
+ it('should convert keyword params to proper form and attaches to request', function () {
+ let bidRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ keywords: {
+ single: 'val',
+ singleArr: ['val'],
+ singleArrNum: [5],
+ multiValMixed: ['value1', 2, 'value3'],
+ singleValNum: 123,
+ emptyStr: '',
+ emptyArr: [''],
+ badValue: {'foo': 'bar'} // should be dropped
+ }
+ }
+ }
+ );
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.tags[0].keywords).to.deep.equal([{
+ 'key': 'single',
+ 'value': ['val']
+ }, {
+ 'key': 'singleArr',
+ 'value': ['val']
+ }, {
+ 'key': 'singleArrNum',
+ 'value': ['5']
+ }, {
+ 'key': 'multiValMixed',
+ 'value': ['value1', '2', 'value3']
+ }, {
+ 'key': 'singleValNum',
+ 'value': ['123']
+ }, {
+ 'key': 'emptyStr'
+ }, {
+ 'key': 'emptyArr'
+ }]);
+ });
+
+ it('should add payment rules to the request', function () {
+ let bidRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ usePaymentRule: true
+ }
+ }
+ );
+
+ const request = spec.buildRequests([bidRequest]);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.tags[0].use_pmt_rule).to.equal(true);
+ });
+
+ it('should add gdpr consent information to the request', function () {
+ let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
+ let bidderRequest = {
+ 'bidderCode': '1ad4good',
+ 'auctionId': '1d1a030790a475',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'timeout': 3000,
+ 'gdprConsent': {
+ consentString: consentString,
+ gdprApplies: true
+ }
+ };
+ bidderRequest.bids = bidRequests;
+
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.gdpr_consent).to.exist;
+ expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString);
+ expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true;
+ });
+
+ it('supports sending hybrid mobile app parameters', function () {
+ let appRequest = Object.assign({},
+ bidRequests[0],
+ {
+ params: {
+ placementId: '10433394',
+ app: {
+ id: 'B1O2W3M4AN.com.prebid.webview',
+ geo: {
+ lat: 40.0964439,
+ lng: -75.3009142
+ },
+ device_id: {
+ idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier
+ aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier
+ md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID
+ sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID
+ windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier
+ }
+ }
+ }
+ }
+ );
+ const request = spec.buildRequests([appRequest]);
+ const payload = JSON.parse(request.data);
+ expect(payload.app).to.exist;
+ expect(payload.app).to.deep.equal({
+ appid: 'B1O2W3M4AN.com.prebid.webview'
+ });
+ expect(payload.device.device_id).to.exist;
+ expect(payload.device.device_id).to.deep.equal({
+ aaid: '38400000-8cf0-11bd-b23e-10b96e40000d',
+ idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE',
+ md5udid: '5756ae9022b2ea1e47d84fead75220c8',
+ sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF',
+ windowsadid: '750c6be243f1c4b5c9912b95a5742fc5'
+ });
+ expect(payload.device.geo).to.exist;
+ expect(payload.device.geo).to.deep.equal({
+ lat: 40.0964439,
+ lng: -75.3009142
+ });
+ });
+
+ it('should add referer info to payload', function () {
+ const bidRequest = Object.assign({}, bidRequests[0])
+ const bidderRequest = {
+ refererInfo: {
+ referer: 'http://example.com/page.html',
+ reachedTop: true,
+ numIframes: 2,
+ stack: [
+ 'http://example.com/page.html',
+ 'http://example.com/iframe1.html',
+ 'http://example.com/iframe2.html'
+ ]
+ }
+ }
+ const request = spec.buildRequests([bidRequest], bidderRequest);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.referrer_detection).to.exist;
+ expect(payload.referrer_detection).to.deep.equal({
+ rd_ref: 'http%3A%2F%2Fexample.com%2Fpage.html',
+ rd_top: true,
+ rd_ifs: 2,
+ rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',')
+ });
+ });
+ })
+
+ describe('interpretResponse', function () {
+ let bfStub;
+ before(function() {
+ bfStub = sinon.stub(bidderFactory, 'getIabSubCategory');
+ });
+
+ after(function() {
+ bfStub.restore();
+ });
+
+ let response = {
+ 'version': '3.0.0',
+ 'tags': [
+ {
+ 'uuid': '3db3773286ee59',
+ 'tag_id': 10433394,
+ 'auction_id': '4534722592064951574',
+ 'nobid': false,
+ 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad',
+ 'timeout_ms': 10000,
+ 'ad_profile_id': 27079,
+ 'ads': [
+ {
+ 'content_source': 'rtb',
+ 'ad_type': 'banner',
+ 'buyer_member_id': 958,
+ 'creative_id': 29681110,
+ 'media_type_id': 1,
+ 'media_subtype_id': 1,
+ 'cpm': 0.5,
+ 'cpm_publisher_currency': 0.5,
+ 'publisher_currency_code': '$',
+ 'client_initiated_ad_counting': true,
+ 'viewability': {
+ 'config': ''
+ },
+ 'rtb': {
+ 'banner': {
+ 'content': '',
+ 'width': 300,
+ 'height': 250
+ },
+ 'trackers': [
+ {
+ 'impression_urls': [
+ 'http://lax1-ib.adnxs.com/impression'
+ ],
+ 'video_events': {}
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ };
+
+ it('should get correct bid response', function () {
+ let expectedResponse = [
+ {
+ 'requestId': '3db3773286ee59',
+ 'cpm': 0.5,
+ 'creativeId': 29681110,
+ 'dealId': undefined,
+ 'width': 300,
+ 'height': 250,
+ 'ad': '',
+ 'mediaType': 'banner',
+ 'currency': 'USD',
+ 'ttl': 300,
+ 'netRevenue': true,
+ 'adUnitCode': 'code',
+ 'ads4good': {
+ 'buyerMemberId': 958
+ }
+ }
+ ];
+ let bidderRequest = {
+ bids: [{
+ bidId: '3db3773286ee59',
+ adUnitCode: 'code'
+ }]
+ }
+ let result = spec.interpretResponse({ body: response }, {bidderRequest});
+ expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0]));
+ });
+
+ it('handles nobid responses', function () {
+ let response = {
+ 'version': '0.0.1',
+ 'tags': [{
+ 'uuid': '84ab500420319d',
+ 'tag_id': 5976557,
+ 'auction_id': '297492697822162468',
+ 'nobid': true
+ }]
+ };
+ let bidderRequest;
+
+ let result = spec.interpretResponse({ body: response }, {bidderRequest});
+ expect(result.length).to.equal(0);
+ });
+
+ it('handles non-banner media responses', function () {
+ let response = {
+ 'tags': [{
+ 'uuid': '84ab500420319d',
+ 'ads': [{
+ 'ad_type': 'video',
+ 'cpm': 0.500000,
+ 'notify_url': 'imptracker.com',
+ 'rtb': {
+ 'video': {
+ 'content': ''
+ }
+ },
+ 'javascriptTrackers': ''
+ }]
+ }]
+ };
+ let bidderRequest = {
+ bids: [{
+ bidId: '84ab500420319d',
+ adUnitCode: 'code'
+ }]
+ }
+
+ let result = spec.interpretResponse({ body: response }, {bidderRequest});
+ expect(result[0]).to.have.property('vastUrl');
+ expect(result[0]).to.have.property('vastImpUrl');
+ expect(result[0]).to.have.property('mediaType', 'video');
+ });
+
+ it('should add deal_priority and deal_code', function() {
+ let responseWithDeal = deepClone(response);
+ responseWithDeal.tags[0].ads[0].deal_priority = 'high';
+ responseWithDeal.tags[0].ads[0].deal_code = '123';
+
+ let bidderRequest = {
+ bids: [{
+ bidId: '3db3773286ee59',
+ adUnitCode: 'code'
+ }]
+ }
+ let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest});
+ expect(Object.keys(result[0].ads4good)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']);
+ });
+
+ it('should add advertiser id', function() {
+ let responseAdvertiserId = deepClone(response);
+ responseAdvertiserId.tags[0].ads[0].advertiser_id = '123';
+
+ let bidderRequest = {
+ bids: [{
+ bidId: '3db3773286ee59',
+ adUnitCode: 'code'
+ }]
+ }
+ let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest});
+ expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']);
+ })
+ });
+});