diff --git a/modules/currency.js b/modules/currency.js new file mode 100644 index 00000000000..1d0286ed569 --- /dev/null +++ b/modules/currency.js @@ -0,0 +1,255 @@ +import bidfactory from 'src/bidfactory'; +import { STATUS } from 'src/constants'; +import { ajax } from 'src/ajax'; +import * as utils from 'src/utils'; +import bidmanager from 'src/bidmanager'; +import { config } from 'src/config'; + +const DEFAULT_CURRENCY_RATE_URL = 'http://currency.prebid.org/latest.json'; +const CURRENCY_RATE_PRECISION = 4; + +var bidResponseQueue = []; +var conversionCache = {}; +var currencyRatesLoaded = false; +var adServerCurrency = 'USD'; + +// Used as reference to the original bidmanager.addBidResponse +var originalBidResponse; + +export var currencySupportEnabled = false; +export var currencyRates = {}; +var bidderCurrencyDefault = {}; + +/** + * Configuration function for currency + * @param {string} [config.adServerCurrency = 'USD'] + * ISO 4217 3-letter currency code that represents the target currency. (e.g. 'EUR'). If this value is present, + * the currency conversion feature is activated. + * @param {number} [config.granularityMultiplier = 1] + * A decimal value representing how mcuh to scale the price granularity calculations. + * @param {object} config.bidderCurrencyDefault + * An optional argument to specify bid currencies for bid adapters. This option is provided for the transitional phase + * before every bid adapter will specify its own bid currency. If the adapter specifies a bid currency, this value is + * ignored for that bidder. + * + * example: + * { + * rubicon: 'USD' + * } + * @param {string} [config.conversionRateFile = 'http://currency.prebid.org/latest.json'] + * Optional path to a file containing currency conversion data. Prebid.org hosts a file that is used as the default, + * if not specified. + * @param {object} [config.rates] + * This optional argument allows you to specify the rates with a JSON object, subverting the need for a external + * config.conversionRateFile parameter. If this argument is specified, the conversion rate file will not be loaded. + * + * example: + * { + * 'GBP': { 'CNY': 8.8282, 'JPY': 141.7, 'USD': 1.2824 }, + * 'USD': { 'CNY': 6.8842, 'GBP': 0.7798, 'JPY': 110.49 } + * } + */ +export function setConfig(config) { + let url = DEFAULT_CURRENCY_RATE_URL; + + if (typeof config.rates === 'object') { + currencyRates.conversions = config.rates; + currencyRatesLoaded = true; + } + + if (typeof config.adServerCurrency === 'string') { + utils.logInfo('enabling currency support', arguments); + + adServerCurrency = config.adServerCurrency; + if (config.conversionRateFile) { + utils.logInfo('currency using override conversionRateFile:', config.conversionRateFile); + url = config.conversionRateFile; + } + initCurrency(url); + } else { + // currency support is disabled, setting defaults + utils.logInfo('disabling currency support'); + resetCurrency(); + } + if (typeof config.bidderCurrencyDefault === 'object') { + bidderCurrencyDefault = config.bidderCurrencyDefault; + } +} +config.getConfig('currency', config => setConfig(config.currency)); + +function initCurrency(url) { + conversionCache = {}; + currencySupportEnabled = true; + + if (!originalBidResponse) { + utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + + originalBidResponse = bidmanager.addBidResponse; + bidmanager.addBidResponse = addBidResponseDecorator(bidmanager.addBidResponse); + } + + if (!currencyRates.conversions) { + ajax(url, function (response) { + try { + currencyRates = JSON.parse(response); + utils.logInfo('currencyRates set to ' + JSON.stringify(currencyRates)); + currencyRatesLoaded = true; + processBidResponseQueue(); + } catch (e) { + utils.logError('failed to parse currencyRates response: ' + response); + } + }); + } +} + +function resetCurrency() { + if (originalBidResponse) { + utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); + + bidmanager.addBidResponse = originalBidResponse; + originalBidResponse = undefined; + } + + adServerCurrency = 'USD'; + conversionCache = {}; + currencySupportEnabled = false; + currencyRatesLoaded = false; + currencyRates = {}; + bidderCurrencyDefault = {}; +} + +export function addBidResponseDecorator(fn) { + return function(adUnitCode, bid) { + if (!bid) { + return fn.apply(this, arguments); // if no bid, call original and let it display warnings + } + + let bidder = bid.bidderCode || bid.bidder; + if (bidderCurrencyDefault[bidder]) { + let currencyDefault = bidderCurrencyDefault[bidder]; + if (bid.currency && currencyDefault !== bid.currency) { + utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); + } else { + bid.currency = currencyDefault; + } + } + + // default to USD if currency not set + if (!bid.currency) { + utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); + bid.currency = 'USD'; + } + + // execute immediately if the bid is already in the desired currency + if (bid.currency === adServerCurrency) { + return fn.apply(this, arguments); + } + + bidResponseQueue.push(wrapFunction(fn, this, arguments)); + if (!currencySupportEnabled || currencyRatesLoaded) { + processBidResponseQueue(); + } + } +} + +function processBidResponseQueue() { + while (bidResponseQueue.length > 0) { + (bidResponseQueue.shift())(); + } +} + +function wrapFunction(fn, context, params) { + return function() { + var bid = params[1]; + if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { + var fromCurrency = bid.currency; + try { + var conversion = getCurrencyConversion(fromCurrency); + bid.originalCpm = bid.cpm; + bid.originalCurrency = bid.currency; + if (conversion !== 1) { + bid.cpm = (parseFloat(bid.cpm) * conversion).toFixed(4); + bid.currency = adServerCurrency; + } + } catch (e) { + utils.logWarn('Returning NO_BID, getCurrencyConversion threw error: ', e); + params[1] = bidfactory.createBid(STATUS.NO_BID, { + bidder: bid.bidderCode || bid.bidder, + bidId: bid.adId + }); + } + } + return fn.apply(context, params); + }; +} + +function getCurrencyConversion(fromCurrency) { + var conversionRate = null; + var rates; + + if (fromCurrency in conversionCache) { + conversionRate = conversionCache[fromCurrency]; + utils.logMessage('Using conversionCache value ' + conversionRate + ' for fromCurrency ' + fromCurrency); + } else if (currencySupportEnabled === false) { + if (fromCurrency === 'USD') { + conversionRate = 1; + } else { + throw new Error('Prebid currency support has not been enabled and fromCurrency is not USD'); + } + } else if (fromCurrency === adServerCurrency) { + conversionRate = 1; + } else { + var toCurrency = adServerCurrency; + + if (fromCurrency in currencyRates.conversions) { + // using direct conversion rate from fromCurrency to toCurrency + rates = currencyRates.conversions[fromCurrency]; + if (!(toCurrency in rates)) { + // bid should fail, currency is not supported + throw new Error('Specified adServerCurrency in config \'' + toCurrency + '\' not found in the currency rates file'); + } + conversionRate = rates[toCurrency]; + utils.logInfo('getCurrencyConversion using direct ' + fromCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate); + } else if (toCurrency in currencyRates.conversions) { + // using reciprocal of conversion rate from toCurrency to fromCurrency + rates = currencyRates.conversions[toCurrency]; + if (!(fromCurrency in rates)) { + // bid should fail, currency is not supported + throw new Error('Specified fromCurrency \'' + fromCurrency + '\' not found in the currency rates file'); + } + conversionRate = roundFloat(1 / rates[fromCurrency], CURRENCY_RATE_PRECISION); + utils.logInfo('getCurrencyConversion using reciprocal ' + fromCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate); + } else { + // first defined currency base used as intermediary + var anyBaseCurrency = Object.keys(currencyRates.conversions)[0]; + + if (!(fromCurrency in currencyRates.conversions[anyBaseCurrency])) { + // bid should fail, currency is not supported + throw new Error('Specified fromCurrency \'' + fromCurrency + '\' not found in the currency rates file'); + } + var toIntermediateConversionRate = 1 / currencyRates.conversions[anyBaseCurrency][fromCurrency]; + + if (!(toCurrency in currencyRates.conversions[anyBaseCurrency])) { + // bid should fail, currency is not supported + throw new Error('Specified adServerCurrency in config \'' + toCurrency + '\' not found in the currency rates file'); + } + var fromIntermediateConversionRate = currencyRates.conversions[anyBaseCurrency][toCurrency]; + + conversionRate = roundFloat(toIntermediateConversionRate * fromIntermediateConversionRate, CURRENCY_RATE_PRECISION); + utils.logInfo('getCurrencyConversion using intermediate ' + fromCurrency + ' thru ' + anyBaseCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate); + } + } + if (!(fromCurrency in conversionCache)) { + utils.logMessage('Adding conversionCache value ' + conversionRate + ' for fromCurrency ' + fromCurrency); + conversionCache[fromCurrency] = conversionRate; + } + return conversionRate; +} + +function roundFloat(num, dec) { + var d = 1; + for (let i = 0; i < dec; i++) { + d += '0'; + } + return Math.round(num * d) / d; +} diff --git a/src/bidmanager.js b/src/bidmanager.js index 60ae0c1a7ca..1842a6864b5 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -178,7 +178,11 @@ exports.addBidResponse = function (adUnitCode, bid) { bid.renderer.setRender(adUnitRenderer.render); } - const priceStringsObj = getPriceBucketString(bid.cpm, config.getConfig('customPriceBucket')); + const priceStringsObj = getPriceBucketString( + bid.cpm, + config.getConfig('customPriceBucket'), + config.getConfig('currency.granularityMultiplier') + ); bid.pbLg = priceStringsObj.low; bid.pbMg = priceStringsObj.med; bid.pbHg = priceStringsObj.high; diff --git a/src/config.js b/src/config.js index 42995369ce9..66507db0847 100644 --- a/src/config.js +++ b/src/config.js @@ -7,7 +7,7 @@ * Defining and access properties in this way is now deprecated, but these will * continue to work during a deprecation window. */ -import { isValidePriceConfig } from './cpmBucketManager'; +import { isValidPriceConfig } from './cpmBucketManager'; const utils = require('./utils'); const DEFAULT_DEBUG = false; @@ -134,7 +134,7 @@ export function newConfig() { utils.logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using `medium` as default.'); } } else if (typeof val === 'object') { - if (!isValidePriceConfig(val)) { + if (!isValidPriceConfig(val)) { utils.logError('Invalid custom price value passed to `setPriceGranularity()`'); return false; } @@ -144,8 +144,8 @@ export function newConfig() { /* * Returns configuration object if called without parameters, - * or single configuration property if given a string matching a configuartion - * property name. + * or single configuration property if given a string matching a configuration + * property name. Allows deep access e.g. getConfig('currency.adServerCurrency') * * If called with callback parameter, or a string and a callback parameter, * subscribes to configuration updates. See `subscribe` function for usage. @@ -153,7 +153,7 @@ export function newConfig() { function getConfig(...args) { if (args.length <= 1 && typeof args[0] !== 'function') { const option = args[0]; - return option ? config[option] : config; + return option ? utils.deepAccess(config, option) : config; } return subscribe(...args); diff --git a/src/cpmBucketManager.js b/src/cpmBucketManager.js index 5728601d245..87daa9488ed 100644 --- a/src/cpmBucketManager.js +++ b/src/cpmBucketManager.js @@ -57,26 +57,25 @@ const _autoPriceConfig = { }] }; -function getPriceBucketString(cpm, customConfig) { - let cpmFloat = 0; - cpmFloat = parseFloat(cpm); +function getPriceBucketString(cpm, customConfig, granularityMultiplier = 1) { + let cpmFloat = parseFloat(cpm); if (isNaN(cpmFloat)) { cpmFloat = ''; } return { - low: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _lgPriceConfig), - med: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _mgPriceConfig), - high: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _hgPriceConfig), - auto: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _autoPriceConfig), - dense: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _densePriceConfig), - custom: (cpmFloat === '') ? '' : getCpmStringValue(cpm, customConfig) + low: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _lgPriceConfig, granularityMultiplier), + med: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _mgPriceConfig, granularityMultiplier), + high: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _hgPriceConfig, granularityMultiplier), + auto: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _autoPriceConfig, granularityMultiplier), + dense: (cpmFloat === '') ? '' : getCpmStringValue(cpm, _densePriceConfig, granularityMultiplier), + custom: (cpmFloat === '') ? '' : getCpmStringValue(cpm, customConfig, granularityMultiplier) }; } -function getCpmStringValue(cpm, config) { +function getCpmStringValue(cpm, config, granularityMultiplier) { let cpmStr = ''; - if (!isValidePriceConfig(config)) { + if (!isValidPriceConfig(config)) { return cpmStr; } const cap = config.buckets.reduce((prev, curr) => { @@ -88,20 +87,20 @@ function getCpmStringValue(cpm, config) { 'max': 0, }); let bucket = config.buckets.find(bucket => { - if (cpm > cap.max) { + if (cpm > cap.max * granularityMultiplier) { const precision = bucket.precision || _defaultPrecision; - cpmStr = bucket.max.toFixed(precision); - } else if (cpm <= bucket.max && cpm >= bucket.min) { + cpmStr = (bucket.max * granularityMultiplier).toFixed(precision); + } else if (cpm <= bucket.max * granularityMultiplier && cpm >= bucket.min * granularityMultiplier) { return bucket; } }); if (bucket) { - cpmStr = getCpmTarget(cpm, bucket.increment, bucket.precision); + cpmStr = getCpmTarget(cpm, bucket.increment, bucket.precision, granularityMultiplier); } return cpmStr; } -function isValidePriceConfig(config) { +function isValidPriceConfig(config) { if (utils.isEmpty(config) || !config.buckets || !Array.isArray(config.buckets)) { return false; } @@ -114,12 +113,12 @@ function isValidePriceConfig(config) { return isValid; } -function getCpmTarget(cpm, increment, precision) { +function getCpmTarget(cpm, increment, precision, granularityMultiplier) { if (!precision) { precision = _defaultPrecision; } - let bucketSize = 1 / increment; + let bucketSize = 1 / increment * granularityMultiplier; return (Math.floor(cpm * bucketSize) / bucketSize).toFixed(precision); } -export { getPriceBucketString, isValidePriceConfig }; +export { getPriceBucketString, isValidPriceConfig }; diff --git a/src/utils.js b/src/utils.js index 6401e713436..0c1846b18ce 100644 --- a/src/utils.js +++ b/src/utils.js @@ -679,3 +679,20 @@ export function groupBy(xs, key) { return rv; }, {}); } + +/** + * deepAccess utility function useful for doing safe access (will not throw exceptions) of deep object paths. + * @param {object} obj The object containing the values you would like to access. + * @param {string|number} path Object path to the value you would like to access. Non-strings are coerced to strings. + * @returns {*} The value found at the specified object path, or undefined if path is not found. + */ +export function deepAccess(obj, path) { + path = String(path).split('.'); + for (let i = 0; i < path.length; i++) { + obj = obj[path[i]]; + if (typeof obj === 'undefined') { + return; + } + } + return obj; +} diff --git a/test/fixtures/fixtures.js b/test/fixtures/fixtures.js index 59b812acc5c..8108da3c555 100644 --- a/test/fixtures/fixtures.js +++ b/test/fixtures/fixtures.js @@ -1370,3 +1370,13 @@ export function getBidRequestedPayload() { 'start': 1465426155412 }; } + +export function getCurrencyRates() { + return { + 'dataAsOf': '2017-04-25', + 'conversions': { + 'GBP': { 'CNY': 8.8282, 'JPY': 141.7, 'USD': 1.2824 }, + 'USD': { 'CNY': 6.8842, 'GBP': 0.7798, 'JPY': 110.49 } + } + }; +} diff --git a/test/spec/cpmBucketManager_spec.js b/test/spec/cpmBucketManager_spec.js index 1590a647417..dd810f29b54 100644 --- a/test/spec/cpmBucketManager_spec.js +++ b/test/spec/cpmBucketManager_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import {getPriceBucketString, isValidePriceConfig} from 'src/cpmBucketManager'; +import {getPriceBucketString, isValidPriceConfig} from 'src/cpmBucketManager'; let cpmFixtures = require('test/fixtures/cpmInputsOutputs.json'); describe('cpmBucketManager', () => { @@ -35,6 +35,29 @@ describe('cpmBucketManager', () => { expect(JSON.stringify(output)).to.deep.equal(expected); }); + it('gets the correct custom bucket strings in non-USD currency', () => { + let cpm = 16.50908 * 110.49; + let customConfig = { + 'buckets': [{ + 'precision': 4, + 'min': 0, + 'max': 3, + 'increment': 0.01, + }, + { + 'precision': 4, + 'min': 3, + 'max': 18, + 'increment': 0.05, + 'cap': true + } + ] + }; + let expected = '{"low":"552.45","med":"1824.09","high":"1824.09","auto":"1824.09","dense":"1824.09","custom":"1824.0882"}'; + let output = getPriceBucketString(cpm, customConfig, 110.49); + expect(JSON.stringify(output)).to.deep.equal(expected); + }); + it('checks whether custom config is valid', () => { let badConfig = { 'buckets': [{ @@ -51,6 +74,6 @@ describe('cpmBucketManager', () => { ] }; - expect(isValidePriceConfig(badConfig)).to.be.false; + expect(isValidPriceConfig(badConfig)).to.be.false; }); }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js new file mode 100644 index 00000000000..f11ef18a35a --- /dev/null +++ b/test/spec/modules/currency_spec.js @@ -0,0 +1,248 @@ + +import { + getCurrencyRates +} from 'test/fixtures/fixtures'; + +import { + setConfig, + addBidResponseDecorator, + + currencySupportEnabled, + currencyRates +} from 'modules/currency'; + +var assert = require('chai').assert; +var expect = require('chai').expect; + +describe('currency', function () { + describe('setConfig', () => { + it('results in currencySupportEnabled = false when currency not configured', () => { + setConfig({}); + expect(currencySupportEnabled).to.equal(false); + }); + + it('results in currencySupportEnabled = true and currencyRates being loaded when configured', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'JPY' }); + fakeCurrencyFileServer.respond(); + expect(currencyRates.dataAsOf).to.equal('2017-04-25'); + expect(currencySupportEnabled).to.equal(true); + }); + }); + + describe('bidder override', () => { + it('allows setConfig to set bidder currency', () => { + setConfig({}); + + var bid = { cpm: 1, bidder: 'rubicon' }; + var innerBid; + + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + + setConfig({ + adServerCurrency: 'GBP', + bidderCurrencyDefault: { + rubicon: 'GBP' + } + }); + + wrappedAddBidResponseFn('elementId', bid); + + expect(innerBid.currency).to.equal('GBP') + }); + + it('uses adapter currency over currency override if specified', () => { + setConfig({}); + + var bid = { cpm: 1, currency: 'JPY', bidder: 'rubicon' }; + var innerBid; + + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + + setConfig({ + adServerCurrency: 'JPY', + bidderCurrencyDefault: { + rubicon: 'GBP' + } + }); + + wrappedAddBidResponseFn('elementId', bid); + + expect(innerBid.currency).to.equal('JPY') + }); + + it('uses rates specified in json when provided', () => { + setConfig({ + adServerCurrency: 'USD', + rates: { + USD: { + JPY: 100 + } + } + }); + + var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; + var innerBid; + + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + + wrappedAddBidResponseFn('elementId', bid); + + expect(innerBid.cpm).to.equal('1.0000'); + }); + }); + + describe('currency.addBidResponseDecorator bidResponseQueue', () => { + it('not run until currency rates file is loaded', () => { + setConfig({}); + + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + + var marker = false; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + marker = true; + }); + var bid = { 'cpm': 1, 'currency': 'USD' }; + + setConfig({ 'adServerCurrency': 'JPY' }); + wrappedAddBidResponseFn('elementId', bid); + expect(marker).to.equal(false); + + fakeCurrencyFileServer.respond(); + expect(marker).to.equal(true); + }); + }); + + describe('currency.addBidResponseDecorator', () => { + it('should leave bid at 1 when currency support is not enabled and fromCurrency is USD', () => { + setConfig({}); + var bid = { 'cpm': 1, 'currency': 'USD' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.cpm).to.equal(1); + }); + + it('should result in NO_BID when currency support is not enabled and fromCurrency is not USD', () => { + setConfig({}); + var bid = { 'cpm': 1, 'currency': 'GBP' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }); + + it('should not buffer bid when currency is already in desired currency', () => { + setConfig({ + 'adServerCurrency': 'USD' + }); + var bid = { 'cpm': 1, 'currency': 'USD' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(bid).to.equal(innerBid); + }); + + it('should result in NO_BID when fromCurrency is not supported in file', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'JPY' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'ABC' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }); + + it('should result in NO_BID when adServerCurrency is not supported in file', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'ABC' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'GBP' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }); + + it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'JPY' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'JPY' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.cpm).to.equal(1); + expect(innerBid.currency).to.equal('JPY'); + }); + + it('should return direct conversion rate when fromCurrency is one of the configured bases', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'GBP' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'USD' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.cpm).to.equal('0.7798'); + expect(innerBid.currency).to.equal('GBP'); + }); + + it('should return reciprocal conversion rate when adServerCurrency is one of the configured bases, but fromCurrency is not', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'GBP' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'CNY' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.cpm).to.equal('0.1133'); + expect(innerBid.currency).to.equal('GBP'); + }); + + it('should return intermediate conversion rate when neither fromCurrency nor adServerCurrency is one of the configured bases', () => { + var fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ 'adServerCurrency': 'CNY' }); + fakeCurrencyFileServer.respond(); + var bid = { 'cpm': 1, 'currency': 'JPY' }; + var innerBid; + var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + innerBid = bid; + }); + wrappedAddBidResponseFn('elementId', bid); + expect(innerBid.cpm).to.equal('0.0623'); + expect(innerBid.currency).to.equal('CNY'); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 2bbdcbaf6e6..e8cb041ebf2 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -524,4 +524,34 @@ describe('Utils', function () { assert.equal(arr.length, count, 'Polyfill test fails') }); }); + + describe('deepAccess', function() { + var obj = { + 1: 2, + test: { + first: 11 + } + }; + + it('should allow deep access of object properties', function() { + var value1 = utils.deepAccess(obj, 'test'); + assert.deepEqual(value1, obj.test); + + var value2 = utils.deepAccess(obj, 'test.first'); + assert.equal(value2, 11); + + var value3 = utils.deepAccess(obj, 1); + assert.equal(value3, 2); + }); + + it('should allow safe access (returning undefined for missing properties and not throwing exceptions)', function() { + var value; + + assert.doesNotThrow(function() { + value = utils.deepAccess(obj, 'test.second.third'); + }); + + assert.equal(value, undefined); + }); + }); });