From 387f1c9728c8afff3877a056f40f65938a60c290 Mon Sep 17 00:00:00 2001 From: Vlad Gurgov Date: Sun, 10 Nov 2019 23:26:28 -0800 Subject: [PATCH] Add 1ad4good bidder (#4081) * adding bidder code and A bidder for non-profit free ads. more info about this bidder project can be found on project site http://1ad4good.org * removed unused code test coverage is improved to >80% tested for instream video support * removed some legacy code, unused params * hardcoding https to endpoint --- modules/1ad4goodBidAdapter.js | 399 ++++++++++++++ modules/1ad4goodBidAdapter.md | 87 +++ test/spec/modules/1ad4goodBidAdapter_spec.js | 548 +++++++++++++++++++ 3 files changed, 1034 insertions(+) create mode 100644 modules/1ad4goodBidAdapter.js create mode 100644 modules/1ad4goodBidAdapter.md create mode 100644 test/spec/modules/1ad4goodBidAdapter_spec.js 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']); + }) + }); +});