From efb5898995d19919b42cfcdfec36be6d8f80880b Mon Sep 17 00:00:00 2001 From: bjorn-lw <32431346+bjorn-lw@users.noreply.github.com> Date: Tue, 9 Oct 2018 16:50:54 +0200 Subject: [PATCH] Livewrapped bid and analytics adapter (#3157) * Livewrapped bid and analytics adapter * Fixed some tests for browser compatibility * Fixed some tests for browser compatibility * Changed analytics adapter code name * Fix double quote in debug message --- modules/livewrappedAnalyticsAdapter.js | 222 +++++++ modules/livewrappedBidAdapter.js | 187 ++++++ modules/livewrappedBidAdapter.md | 28 + .../livewrappedAnalyticsAdapter_spec.js | 265 ++++++++ .../modules/livewrappedBidAdapter_spec.js | 609 ++++++++++++++++++ 5 files changed, 1311 insertions(+) create mode 100644 modules/livewrappedAnalyticsAdapter.js create mode 100644 modules/livewrappedBidAdapter.js create mode 100644 modules/livewrappedBidAdapter.md create mode 100644 test/spec/modules/livewrappedAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/livewrappedBidAdapter_spec.js diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js new file mode 100644 index 00000000000..21c0bc73ec4 --- /dev/null +++ b/modules/livewrappedAnalyticsAdapter.js @@ -0,0 +1,222 @@ +import * as utils from 'src/utils'; +import {ajax} from 'src/ajax'; +import adapter from 'src/AnalyticsAdapter'; +import CONSTANTS from 'src/constants.json'; +import adaptermanager from 'src/adaptermanager'; + +const ANALYTICSTYPE = 'endpoint'; +const URL = '//lwadm.com/analytics/10'; +const EMPTYURL = ''; +const REQUESTSENT = 1; +const RESPONSESENT = 2; +const WINSENT = 4; +const TIMEOUTSENT = 8; + +let initOptions; +export const BID_WON_TIMEOUT = 500; + +const cache = { + auctions: {}, +}; + +let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE}), { + track({eventType, args}) { + utils.logInfo('LIVEWRAPPED_EVENT:', [eventType, args]); + + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + utils.logInfo('LIVEWRAPPED_AUCTION_INIT:', args); + cache.auctions[args.auctionId] = {bids: {}}; + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + utils.logInfo('LIVEWRAPPED_BID_REQUESTED:', args); + + args.bids.forEach(function(bidRequest) { + cache.auctions[args.auctionId].timeStamp = args.start; + cache.auctions[args.auctionId].bids[bidRequest.bidId] = { + bidder: bidRequest.bidder, + adUnit: bidRequest.adUnitCode, + isBid: false, + won: false, + timeout: false, + sendStatus: 0 + } + + utils.logInfo(bidRequest); + }) + utils.logInfo(livewrappedAnalyticsAdapter.requestEvents); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + utils.logInfo('LIVEWRAPPED_BID_RESPONSE:', args); + + let bidResponse = cache.auctions[args.auctionId].bids[args.adId]; + bidResponse.isBid = args.getStatusCode() === CONSTANTS.STATUS.GOOD; + bidResponse.width = args.width; + bidResponse.height = args.height; + bidResponse.cpm = args.cpm; + bidResponse.ttr = args.timeToRespond; + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + utils.logInfo('LIVEWRAPPED_BIDDER_DONE:', args); + args.bids.forEach(doneBid => { + let bid = cache.auctions[doneBid.auctionId].bids[doneBid.bidId]; + if (!bid.ttr) { + bid.ttr = Date.now() - args.auctionStart; + } + }); + break; + case CONSTANTS.EVENTS.BID_WON: + utils.logInfo('LIVEWRAPPED_BID_WON:', args); + let wonBid = cache.auctions[args.auctionId].bids[args.adId]; + wonBid.won = true; + if (wonBid.sendStatus != 0) { + livewrappedAnalyticsAdapter.sendEvents(); + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + utils.logInfo('LIVEWRAPPED_BID_TIMEOUT:', args); + args.forEach(timeout => { + cache.auctions[timeout.auctionId].bids[timeout.bidId].timeout = true; + }); + break; + case CONSTANTS.EVENTS.AUCTION_END: + utils.logInfo('LIVEWRAPPED_AUCTION_END:', args); + setTimeout(() => { + livewrappedAnalyticsAdapter.sendEvents(); + }, BID_WON_TIMEOUT); + break; + } + } +}); + +// save the base class function +livewrappedAnalyticsAdapter.originEnableAnalytics = livewrappedAnalyticsAdapter.enableAnalytics; +livewrappedAnalyticsAdapter.allRequestEvents = []; + +// override enableAnalytics so we can get access to the config passed in from the page +livewrappedAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + livewrappedAnalyticsAdapter.originEnableAnalytics(config); +}; + +livewrappedAnalyticsAdapter.sendEvents = function() { + var events = { + publisherId: initOptions.publisherId, + requests: getSentRequests(), + responses: getResponses(), + wins: getWins(), + timeouts: getTimeouts() + }; + + if (events.requests.length == 0 && + events.responses.length == 0 && + events.wins.length == 0 && + events.timeouts.length == 0) { + return; + } + + ajax(URL, undefined, JSON.stringify(events), {method: 'POST'}); +} + +function getSentRequests() { + var sentRequests = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let auction = cache.auctions[auctionId]; + let bid = auction.bids[bidId]; + if (!(bid.sendStatus & REQUESTSENT)) { + bid.sendStatus |= REQUESTSENT; + + sentRequests.push({ + timeStamp: auction.timeStamp, + adUnit: bid.adUnit, + bidder: bid.bidder + }); + } + }); + }); + + return sentRequests; +} + +function getResponses() { + var responses = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let auction = cache.auctions[auctionId]; + let bid = auction.bids[bidId]; + if (!(bid.sendStatus & RESPONSESENT) && !bid.timeout) { + bid.sendStatus |= RESPONSESENT; + + responses.push({ + timeStamp: auction.timeStamp, + adUnit: bid.adUnit, + bidder: bid.bidder, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + ttr: bid.ttr, + IsBid: bid.isBid + }); + } + }); + }); + + return responses; +} + +function getWins() { + var wins = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let auction = cache.auctions[auctionId]; + let bid = auction.bids[bidId]; + if (!(bid.sendStatus & WINSENT) && bid.won) { + bid.sendStatus |= WINSENT; + + wins.push({ + timeStamp: auction.timeStamp, + adUnit: bid.adUnit, + bidder: bid.bidder, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + }); + } + }); + }); + + return wins; +} + +function getTimeouts() { + var timeouts = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let auction = cache.auctions[auctionId]; + let bid = auction.bids[bidId]; + if (!(bid.sendStatus & TIMEOUTSENT) && bid.timeout) { + bid.sendStatus |= TIMEOUTSENT; + + timeouts.push({ + bidder: bid.bidder, + adUnit: bid.adUnit, + timeStamp: auction.timeStamp + }); + } + }); + }); + + return timeouts; +} + +adaptermanager.registerAnalyticsAdapter({ + adapter: livewrappedAnalyticsAdapter, + code: 'livewrapped' +}); + +export default livewrappedAnalyticsAdapter; diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js new file mode 100644 index 00000000000..ebfe4aad2d4 --- /dev/null +++ b/modules/livewrappedBidAdapter.js @@ -0,0 +1,187 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import find from 'core-js/library/fn/array/find'; + +const BIDDER_CODE = 'livewrapped'; +export const URL = 'https://lwadm.com/ad'; +const VERSION = '1.1'; + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. + * + * Parameters should be + * + * adUnitId: LiveWrapped's id of the ad unit. Optional. A guid identifying the ad unit. + * adUnitName: LiveWrapped's name of the ad unit Optional. (Prebid's ad unit code will be used otherwise.) + * publisherId: Publisher id. Required if adUnitName is used or both adUnitName and adUnitId is omitted, otherwise optional. + * userId: A persistent user id if available. Optional. + * url: Page url Optional. Use if page url cannot be determined due to use of iframes. + * bidUrl: Bidding endpoint Optional. + * seats: List of bidders and seats Optional. {"bidder name": ["seat 1", "seat 2"], ...} + * deviceId: Device id if available Optional. + * ifa: Advertising ID Optional. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return (bid.params.adUnitId || ((bid.params.adUnitName || bid.adUnitCode || bid.placementCode) && bid.params.publisherId)) !== undefined; + }, + + /** + * 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 userId = find(bidRequests, hasUserId); + const publisherId = find(bidRequests, hasPublisherId); + const auctionId = find(bidRequests, hasAuctionId); + let bidUrl = find(bidRequests, hasBidUrl); + let url = find(bidRequests, hasUrl); + let test = find(bidRequests, hasTestParam); + let seats = find(bidRequests, hasSeatsParam); + let deviceId = find(bidRequests, hasDeviceIdParam); + let ifa = find(bidRequests, hasIfaParam); + let tid = find(bidRequests, hasTidParam); + bidUrl = bidUrl ? bidUrl.params.bidUrl : URL; + url = url ? url.params.url : (config.getConfig('pageUrl') || utils.getTopWindowUrl()); + test = test ? test.params.test : undefined; + var adRequests = bidRequests.map(bidToAdRequest); + + const payload = { + auctionId: auctionId ? auctionId.auctionId : undefined, + publisherId: publisherId ? publisherId.params.publisherId : undefined, + userId: userId ? userId.params.userId : undefined, + url: url, + test: test, + seats: seats ? seats.params.seats : undefined, + deviceId: deviceId ? deviceId.params.deviceId : undefined, + ifa: ifa ? ifa.params.ifa : undefined, + tid: tid ? tid.params.tid : undefined, + version: VERSION, + gdprApplies: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : false, + gdprConsent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined, + cookieSupport: !utils.isSafariBrowser() && utils.cookiesAreEnabled(), + adRequests: [...adRequests] + }; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: bidUrl, + data: payloadString, + }; + }, + + /** + * 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) { + const bidResponses = []; + + serverResponse.body.ads.forEach(function(ad) { + let bidResponse = { + requestId: ad.bidId, + bidderCode: BIDDER_CODE, + cpm: ad.cpmBid, + width: ad.width, + height: ad.height, + ad: ad.tag, + ttl: ad.ttl, + creativeId: ad.creativeId, + netRevenue: true, + currency: serverResponse.body.currency + }; + + bidResponses.push(bidResponse); + }); + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + if (serverResponses.length == 0) return []; + + let syncList = []; + let userSync = serverResponses[0].body.pixels || []; + + userSync.forEach(function(sync) { + if (syncOptions.pixelEnabled && sync.type == 'Redirect') { + syncList.push({type: 'image', url: sync.url}); + } + + if (syncOptions.iframeEnabled && sync.type == 'Iframe') { + syncList.push({type: 'iframe', url: sync.url}); + } + }); + + return syncList; + } +} + +function hasUserId(bid) { + return !!bid.params.userId; +} + +function hasPublisherId(bid) { + return !!bid.params.publisherId; +} + +function hasUrl(bid) { + return !!bid.params.url; +} + +function hasBidUrl(bid) { + return !!bid.params.bidUrl; +} + +function hasAuctionId(bid) { + return !!bid.auctionId; +} + +function hasTestParam(bid) { + return !!bid.params.test; +} + +function hasSeatsParam(bid) { + return !!bid.params.seats; +} + +function hasDeviceIdParam(bid) { + return !!bid.params.deviceId; +} + +function hasIfaParam(bid) { + return !!bid.params.ifa; +} + +function hasTidParam(bid) { + return !!bid.params.tid; +} + +function bidToAdRequest(bid) { + return { + adUnitId: bid.params.adUnitId, + callerAdUnitId: bid.params.adUnitName || bid.adUnitCode || bid.placementCode, + bidId: bid.bidId, + transactionId: bid.transactionId, + formats: bid.sizes.map(sizeToFormat) + }; +} + +function sizeToFormat(size) { + return { + width: size[0], + height: size[1] + } +} + +registerBidder(spec); diff --git a/modules/livewrappedBidAdapter.md b/modules/livewrappedBidAdapter.md new file mode 100644 index 00000000000..15e3e8d533a --- /dev/null +++ b/modules/livewrappedBidAdapter.md @@ -0,0 +1,28 @@ +# Overview + +**Module Name**: Livewrapped Bid Adapter +**Module Type**: Bidder Adapter +**Maintainer**: info@livewrapped.com + +# Description + +Connects to Livewrapped Header Bidding wrapper for bids. + +Livewrapped supports banner. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'banner-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'livewrapped', + params: { + adUnitId: '6A32352E-BC17-4B94-B2A7-5BF1724417D7' + } + }] + } +]; +``` diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..f7de9cd8101 --- /dev/null +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -0,0 +1,265 @@ +import livewrappedAnalyticsAdapter, { BID_WON_TIMEOUT } from 'modules/livewrappedAnalyticsAdapter'; +import CONSTANTS from 'src/constants.json'; +import { config } from 'src/config'; + +let events = require('src/events'); +let adaptermanager = require('src/adaptermanager'); + +const { + EVENTS: { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + BIDDER_DONE, + BID_WON, + BID_TIMEOUT, + SET_TARGETING + }, + STATUS: { + GOOD + } +} = CONSTANTS; + +const BID1 = { + width: 980, + height: 240, + cpm: 1.1, + timeToRespond: 200, + bidId: '2ecff0db240757', + adId: '2ecff0db240757', + auctionId: '25c6d7f5-699a-4bfc-87c9-996f915341fa', + getStatusCode() { + return CONSTANTS.STATUS.GOOD; + } +}; + +const BID2 = Object.assign({}, BID1, { + width: 300, + height: 250, + cpm: 2.2, + timeToRespond: 300, + bidId: '3ecff0db240757', + adId: '3ecff0db240757', +}); + +const MOCK = { + AUCTION_INIT: { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + }, + BID_REQUESTED: { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + }, + { + 'bidder': 'livewrapped', + 'adUnitCode': 'box_d_1', + 'bidId': '3ecff0db240757', + } + ], + 'start': 1519149562216 + }, + BID_RESPONSE: [ + BID1, + BID2 + ], + AUCTION_END: { + }, + BID_WON: [ + Object.assign({}, BID1, { + 'status': 'rendered' + }), + Object.assign({}, BID2, { + 'status': 'rendered' + }) + ], + BIDDER_DONE: { + 'bidderCode': 'livewrapped', + 'bids': [ + BID1, + BID2 + ] + }, + BID_TIMEOUT: [ + { + 'bidId': '2ecff0db240757', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ] +}; + +const ANALYTICS_MESSAGE = { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + requests: [ + { + adUnit: 'panorama_d_1', + bidder: 'livewrapped', + timeStamp: 1519149562216 + }, + { + adUnit: 'box_d_1', + bidder: 'livewrapped', + timeStamp: 1519149562216 + } + ], + responses: [ + { + timeStamp: 1519149562216, + adUnit: 'panorama_d_1', + bidder: 'livewrapped', + width: 980, + height: 240, + cpm: 1.1, + ttr: 200, + IsBid: true + }, + { + timeStamp: 1519149562216, + adUnit: 'box_d_1', + bidder: 'livewrapped', + width: 300, + height: 250, + cpm: 2.2, + ttr: 300, + IsBid: true + } + ], + timeouts: [], + wins: [ + { + timeStamp: 1519149562216, + adUnit: 'panorama_d_1', + bidder: 'livewrapped', + width: 980, + height: 240, + cpm: 1.1 + }, + { + timeStamp: 1519149562216, + adUnit: 'box_d_1', + bidder: 'livewrapped', + width: 300, + height: 250, + cpm: 2.2 + } + ] +}; + +function performStandardAuction() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); +} + +describe('Livewrapped analytics adapter', function () { + let sandbox; + let xhr; + let requests; + let clock; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + + sandbox.stub(events, 'getEvents').returns([]); + + clock = sandbox.useFakeTimers(1519767013781); + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + }); + + describe('when handling events', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'livewrapped', + adapter: livewrappedAnalyticsAdapter + }); + + beforeEach(function () { + adaptermanager.enableAnalytics({ + provider: 'livewrapped', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7' + } + }); + }); + + afterEach(function () { + livewrappedAnalyticsAdapter.disableAnalytics(); + }); + + it('should build a batched message from prebid events', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(requests.length).to.equal(1); + let request = requests[0]; + + expect(request.url).to.equal('//lwadm.com/analytics/10'); + + let message = JSON.parse(request.requestBody); + + expect(message).to.deep.equal(ANALYTICS_MESSAGE); + }); + + it('should send batched message without BID_WON if necessary and further BID_WON events individually', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(BID_WON_TIMEOUT + 1000); + + events.emit(BID_WON, MOCK.BID_WON[1]); + + expect(requests.length).to.equal(2); + + let message = JSON.parse(requests[0].requestBody); + expect(message.wins.length).to.equal(1); + expect(message.requests).to.deep.equal(ANALYTICS_MESSAGE.requests); + expect(message.wins[0]).to.deep.equal(ANALYTICS_MESSAGE.wins[0]); + + message = JSON.parse(requests[1].requestBody); + expect(message.wins.length).to.equal(1); + expect(message.wins[0]).to.deep.equal(ANALYTICS_MESSAGE.wins[1]); + }); + + it('should properly mark bids as timed out', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(requests.length).to.equal(1); + + let message = JSON.parse(requests[0].requestBody); + expect(message.timeouts.length).to.equal(1); + expect(message.timeouts[0].bidder).to.equal('livewrapped'); + expect(message.timeouts[0].adUnit).to.equal('panorama_d_1'); + }); + }); +}); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js new file mode 100644 index 00000000000..64a70b946ef --- /dev/null +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -0,0 +1,609 @@ +import {expect} from 'chai'; +import {spec} from 'modules/livewrappedBidAdapter'; +import {config} from 'src/config'; +import * as utils from 'src/utils'; + +describe('Livewrapped adapter tests', function () { + let sandbox, + bidderRequest; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + bidderRequest = { + bidderCode: 'livewrapped', + auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + bidderRequestId: '178e34bad3658f', + bids: [ + { + bidder: 'livewrapped', + params: { + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']} + }, + adUnitCode: 'panorama_d_1', + sizes: [[980, 240], [980, 120]], + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + } + ], + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000 + }; + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('isBidRequestValid', function() { + it('should accept a request with id only as valid', function() { + let bid = {params: {adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37'}}; + + let result = spec.isBidRequestValid(bid); + + expect(result).to.be.true; + }); + + it('should accept a request with adUnitName and PublisherId as valid', function() { + let bid = {params: {adUnitName: 'panorama_d_1', publisherId: '26947112-2289-405D-88C1-A7340C57E63E'}}; + + let result = spec.isBidRequestValid(bid); + + expect(result).to.be.true; + }); + + it('should accept a request with adUnitCode and PublisherId as valid', function() { + let bid = {adUnitCode: 'panorama_d_1', params: {publisherId: '26947112-2289-405D-88C1-A7340C57E63E'}}; + + let result = spec.isBidRequestValid(bid); + + expect(result).to.be.true; + }); + + it('should accept a request with placementCode and PublisherId as valid', function() { + let bid = {placementCode: 'panorama_d_1', params: {publisherId: '26947112-2289-405D-88C1-A7340C57E63E'}}; + + let result = spec.isBidRequestValid(bid); + + expect(result).to.be.true; + }); + + it('should not accept a request with adUnitName, adUnitCode, placementCode but no PublisherId as valid', function() { + let bid = {placementCode: 'panorama_d_1', adUnitCode: 'panorama_d_1', params: {adUnitName: 'panorama_d_1'}}; + + let result = spec.isBidRequestValid(bid); + + expect(result).to.be.false; + }); + }); + + describe('buildRequests', function() { + it('should make a well-formed single request object', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let result = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed multiple request object', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let multiplebidRequest = clone(bidderRequest); + multiplebidRequest.bids.push(clone(bidderRequest.bids[0])); + multiplebidRequest.bids[1].adUnitCode = 'box_d_1'; + multiplebidRequest.bids[1].sizes = [[300, 250]]; + multiplebidRequest.bids[1].bidId = '3ffb201a808da7'; + delete multiplebidRequest.bids[1].params.adUnitId; + + let result = spec.buildRequests(multiplebidRequest.bids, multiplebidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }, { + callerAdUnitId: 'box_d_1', + bidId: '3ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 300, height: 250}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed single request object with AdUnitName', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + testbidRequest.bids[0].params.adUnitName = 'caller id 1'; + delete testbidRequest.bids[0].params.adUnitId; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + callerAdUnitId: 'caller id 1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed single request object with less parameters', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + delete testbidRequest.bids[0].params.seats; + delete testbidRequest.bids[0].params.adUnitId; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + url: 'http://www.domain.com', + version: '1.1', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed single request object with less parameters, no publisherId', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + delete testbidRequest.bids[0].params.seats; + delete testbidRequest.bids[0].params.publisherId; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + url: 'http://www.domain.com', + version: '1.1', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed single request object with app parameters', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + delete testbidRequest.bids[0].params.seats; + delete testbidRequest.bids[0].params.adUnitId; + testbidRequest.bids[0].params.deviceId = 'deviceid'; + testbidRequest.bids[0].params.ifa = 'ifa'; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + url: 'http://www.domain.com', + version: '1.1', + deviceId: 'deviceid', + ifa: 'ifa', + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should make a well-formed single request object with debug parameters', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + delete testbidRequest.bids[0].params.seats; + delete testbidRequest.bids[0].params.adUnitId; + testbidRequest.bids[0].params.tid = 'tracking id'; + testbidRequest.bids[0].params.test = true; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + url: 'http://www.domain.com', + version: '1.1', + tid: 'tracking id', + test: true, + cookieSupport: true, + gdprApplies: false, + adRequests: [{ + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should pass gdpr parameters', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + let testRequest = clone(bidderRequest); + testRequest.gdprConsent = { + gdprApplies: true, + consentString: 'test' + }; + let result = spec.buildRequests(testRequest.bids, testRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + cookieSupport: true, + gdprApplies: true, + gdprConsent: 'test', + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should pass no cookie support', function() { + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => false); + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + let result = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + gdprApplies: false, + cookieSupport: false, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should pass no cookie support Safari', function() { + sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => true); + let result = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'http://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.1', + gdprApplies: false, + cookieSupport: false, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should use params.url, then config pageUrl, then getTopWindowUrl', function() { + let testRequest = clone(bidderRequest); + sandbox.stub(utils, 'getTopWindowUrl').callsFake(() => 'http://www.topurl.com'); + + let result = spec.buildRequests(testRequest.bids, testRequest); + let data = JSON.parse(result.data); + + expect(data.url).to.equal('http://www.domain.com'); + + delete testRequest.bids[0].params.url; + + result = spec.buildRequests(testRequest.bids, testRequest); + data = JSON.parse(result.data); + + expect(data.url).to.equal('http://www.topurl.com'); + + let origGetConfig = config.getConfig; + sandbox.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'pageUrl') { + return 'http://www.configurl.com'; + } + return origGetConfig.apply(config, arguments); + }); + + result = spec.buildRequests(testRequest.bids, testRequest); + data = JSON.parse(result.data); + + expect(data.url).to.equal('http://www.configurl.com'); + }); + }); + + describe('interpretResponse', function () { + it('should handle single success response', function() { + let lwResponse = { + ads: [ + { + id: '28e5ddf4-3c01-11e8-86a7-0a44794250d4', + callerId: 'site_outsider_0', + tag: 'ad', + width: 300, + height: 250, + cpmBid: 2.565917, + bidId: '32e50fad901ae89', + auctionId: '13e674db-d4d8-4e19-9d28-ff38177db8bf', + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + ttl: 120 + } + ], + currency: 'USD' + }; + + let expectedResponse = [{ + requestId: '32e50fad901ae89', + bidderCode: 'livewrapped', + cpm: 2.565917, + width: 300, + height: 250, + ad: 'ad', + ttl: 120, + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + netRevenue: true, + currency: 'USD' + }]; + + let bids = spec.interpretResponse({body: lwResponse}); + + expect(bids).to.deep.equal(expectedResponse); + }) + + it('should handle multiple success response', function() { + let lwResponse = { + ads: [ + { + id: '28e5ddf4-3c01-11e8-86a7-0a44794250d4', + callerId: 'site_outsider_0', + tag: 'ad1', + width: 300, + height: 250, + cpmBid: 2.565917, + bidId: '32e50fad901ae89', + auctionId: '13e674db-d4d8-4e19-9d28-ff38177db8bf', + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + ttl: 120 + }, + { + id: '38e5ddf4-3c01-11e8-86a7-0a44794250d4', + callerId: 'site_outsider_1', + tag: 'ad2', + width: 980, + height: 240, + cpmBid: 3.565917, + bidId: '42e50fad901ae89', + auctionId: '13e674db-d4d8-4e19-9d28-ff38177db8bf', + creativeId: '62cbd598-2715-4c43-a06f-229fc170f945:427077', + ttl: 120 + } + ], + currency: 'USD' + }; + + let expectedResponse = [{ + requestId: '32e50fad901ae89', + bidderCode: 'livewrapped', + cpm: 2.565917, + width: 300, + height: 250, + ad: 'ad1', + ttl: 120, + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + netRevenue: true, + currency: 'USD' + }, { + requestId: '42e50fad901ae89', + bidderCode: 'livewrapped', + cpm: 3.565917, + width: 980, + height: 240, + ad: 'ad2', + ttl: 120, + creativeId: '62cbd598-2715-4c43-a06f-229fc170f945:427077', + netRevenue: true, + currency: 'USD' + }]; + + let bids = spec.interpretResponse({body: lwResponse}); + + expect(bids).to.deep.equal(expectedResponse); + }) + }); + + describe('user sync', function () { + let serverResponses; + + beforeEach(function () { + serverResponses = [{ + body: { + pixels: [ + {type: 'Redirect', url: 'http://pixelsync'}, + {type: 'Iframe', url: 'http://iframesync'} + ] + } + }]; + }); + + it('should return empty if no server responses', function() { + let syncs = spec.getUserSyncs({ + pixelEnabled: true, + iframeEnabled: true + }, []); + + let expectedResponse = []; + + expect(syncs).to.deep.equal(expectedResponse) + }); + + it('should return empty if no user sync', function() { + let syncs = spec.getUserSyncs({ + pixelEnabled: true, + iframeEnabled: true + }, [{body: {}}]); + + let expectedResponse = []; + + expect(syncs).to.deep.equal(expectedResponse) + }); + + it('should returns pixel and iframe user sync', function() { + let syncs = spec.getUserSyncs({ + pixelEnabled: true, + iframeEnabled: true + }, serverResponses); + + let expectedResponse = [{type: 'image', url: 'http://pixelsync'}, {type: 'iframe', url: 'http://iframesync'}]; + + expect(syncs).to.deep.equal(expectedResponse) + }); + + it('should returns pixel only if iframe not supported user sync', function() { + let syncs = spec.getUserSyncs({ + pixelEnabled: true, + iframeEnabled: false + }, serverResponses); + + let expectedResponse = [{type: 'image', url: 'http://pixelsync'}]; + + expect(syncs).to.deep.equal(expectedResponse) + }); + + it('should returns iframe only if pixel not supported user sync', function() { + let syncs = spec.getUserSyncs({ + pixelEnabled: false, + iframeEnabled: true + }, serverResponses); + + let expectedResponse = [{type: 'iframe', url: 'http://iframesync'}]; + + expect(syncs).to.deep.equal(expectedResponse) + }); + }); +}); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +}