oanda-exchange-rates
Version:
OANDA Exchange Rates API client module for nodejs
714 lines (621 loc) • 25.2 kB
JavaScript
var https = require('https'),
OANDAExchangeRates = require('../index'),
should = require('should'),
sinon = require('sinon');
describe('OANDA Exchange Rates API', function() {
describe('constructor', function() {
describe('when the api key is missing', function() {
var exception = sinon.spy();
before(function() {
try {
var client = new OANDAExchangeRates();
} catch(e) {
exception(e);
}
});
it('throws an exception', function() {
exception.callCount.should.equal(1);
});
it('indicates the api key is missing', function() {
exception.firstCall.args[0].should.equal('No API key specified');
});
});
});
var _createResponse = function(code, data, callback) {
if (typeof(data) === 'string') {
data = [ data ];
}
return {
statusCode: code,
on: function(evt, callback) {
if (evt === 'data') {
data.forEach(function(chunk) {
callback(new Buffer(chunk));
});
}
if (evt === 'end') {
callback();
}
}
}
};
describe('_getResponse()', function() {
describe('when using a proxy URL', function() {
var proxySpy = sinon.spy();
before(function() {
// Arrange
sinon.spy(https, 'get')
var client = new OANDAExchangeRates({
api_key: '42',
proxy: 'http://proxy:8080'
});
client.__proto__._makeProxy = proxySpy;
// Act
client._getResponse('endpoint');
});
// Assert
it('makes a HTTPS GET request', function() {
https.get.callCount.should.equal(1);
});
it('sends the API\'s base URL as hostname', function() {
https.get.firstCall.args[0].should.have.property('hostname', 'www.oanda.com');
});
it('sends the endpoint as path', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/endpoint');
});
it('sends the API_key as Authorization header', function() {
https.get.firstCall.args[0].headers.should.have.property('Authorization', 'Bearer 42');
});
it('sends the module name and version as User-Agent header', function() {
https.get.firstCall.args[0].headers.should.have.property('User-Agent', 'oanda-exchange-rates.js/0.2.0');
});
it('sends a proxy agent as agent', function() {
https.get.firstCall.args[0].should.have.property('agent');
});
it('creates a proxy agent', function() {
proxySpy.callCount.should.equal(1);
});
it('passes a proxy URL to the proxy agent', function() {
proxySpy.firstCall.args[0].should.equal('http://proxy:8080');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('when http_proxy variable is set in the environment', function() {
var proxySpy = sinon.spy();
before(function() {
// Arrange
sinon.spy(https, 'get')
process.env.http_proxy = 'http://proxy:8080';
var client = new OANDAExchangeRates({
api_key: '42'
});
client.__proto__._makeProxy = proxySpy;
// Act
client._getResponse('endpoint');
});
// Assert
it('makes a HTTP GET request', function() {
https.get.callCount.should.equal(1);
});
it('sends the API\'s base URL as hostname', function() {
https.get.firstCall.args[0].should.have.property('hostname', 'www.oanda.com');
});
it('sends the endpoint as path', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/endpoint');
});
it('sends the API_key as Authorization header', function() {
https.get.firstCall.args[0].headers.should.have.property('Authorization', 'Bearer 42');
});
it('sends the module name and version as User-Agent header', function() {
https.get.firstCall.args[0].headers.should.have.property('User-Agent', 'oanda-exchange-rates.js/0.2.0');
});
it('sends a proxy agent as agent', function() {
https.get.firstCall.args[0].should.have.property('agent');
});
it('creates a proxy agent', function() {
proxySpy.callCount.should.equal(1);
});
it('passes the proxy URL to the proxy agent', function() {
proxySpy.firstCall.args[0].should.equal('http://proxy:8080');
});
// Restore
after(function() {
https.get.restore();
delete process.env.http_proxy
});
});
describe('when using the API base URL', function() {
before(function() {
// Arrange
sinon.spy(https, 'get')
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client._getResponse('endpoint');
});
// Assert
it('makes a HTTPS GET request', function() {
https.get.callCount.should.equal(1);
});
it('sends the API\'s base URL as hostname', function() {
https.get.firstCall.args[0].should.have.property('hostname', 'www.oanda.com');
});
it('sends the endpoint as path', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/endpoint');
});
it('sends the API_key as Authorization header', function() {
https.get.firstCall.args[0].headers.should.have.property('Authorization', 'Bearer 42');
});
it('sends the module name and version as User-Agent header', function() {
https.get.firstCall.args[0].headers.should.have.property('User-Agent', 'oanda-exchange-rates.js/0.2.0');
});
it('does not send a HTTP proxy agent as agent', function() {
https.get.firstCall.args[0].should.not.have.property('agent');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('when the request is successful', function() {
describe('and the response is sent in one chunk', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, '{"foo":[{"bar":"fubar","fizzy":"fish"}]}', callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client._getResponse('endpoint', callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes the deserialized JSON back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
foo: [{
bar: 'fubar',
fizzy: 'fish'
}]
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"foo":[{"bar":"fubar","fizzy":"fish"}]}');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('and the response is sent in multiple chunks', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, [
'{"foo":[{"bar":"fubar","fiz',
'zy":"fish"}]}'
], callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client._getResponse('endpoint', callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes the deserialized JSON back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
foo: [{
bar: 'fubar',
fizzy: 'fish'
}]
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"foo":[{"bar":"fubar","fizzy":"fish"}]}');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
});
describe('when the request fails', function() {
describe('due to an API error', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(_createResponse(400,'{"code":8,"message":"Malformed Authorization header or invalid access token"}', callback));
var client = new OANDAExchangeRates({
api_key: 'expired key'
});
// Act
client._getResponse('endpoint', callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a failure flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', false);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 400);
});
it('passes an error code back to our callback', function() {
callback.firstCall.args[0].should.have.property('errorCode', 8);
});
it('passes an error message back to our callback', function() {
callback.firstCall.args[0].should.have.property('errorMessage', 'Malformed Authorization header or invalid access token');
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"code":8,"message":"Malformed Authorization header or invalid access token"}');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('due to malformed JSON data', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(_createResponse(200,'{"foo":[{"bar":"fubar","fizz', callback));
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client._getResponse('endpoint', callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a failure flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', false);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('does not pass an error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('passes an error message back to our callback', function() {
callback.firstCall.args[0].should.have.property('errorMessage', 'Unable to parse JSON data: Unexpected token f');
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"foo":[{"bar":"fubar","fizz');
});
// Restore
after(function() {
https.get.restore();
});
});
});
});
describe('getCurrencies', function() {
var callback = sinon.spy();
describe('when data_set parameter is omitted', function() {
before(function() {
// Arrange
sinon.spy(https, 'get')
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client.getCurrencies();
});
// Assert
it('does not send any query string', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/currencies.json');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('when data_set parameter is passed', function() {
before(function() {
// Arrange
sinon.spy(https, 'get')
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client.getCurrencies('ecb');
});
// Assert
it('sends the data_set value in the query string', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/currencies.json?data_set=ecb');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('when the response comes back', function() {
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, '{"currencies":[{"code":"ADF","description":"Andorran Franc"}]}', callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client.getCurrencies(callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes a hash of currencies code/description back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
ADF: 'Andorran Franc'
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"currencies":[{"code":"ADF","description":"Andorran Franc"}]}');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
});
describe('getRemainingQuotes', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, '{"remaining_quotes":100000}', callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client.getRemainingQuotes(callback);
});
// Assert
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes a hash with a remaining quotes key/value back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
remaining_quotes: 100000
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"remaining_quotes":100000}');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('getRates', function() {
describe('when only the base currency is passed', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, '{"quotes":{"CAD":{"date":"2014-05-05T21:00:00+0000","bid":"1.09681","ask":"1.09702"}},"meta":{"skipped_currencies":[],"request_time":"2014-05-06T18:27:56+0000","effective_params":{"quote_currencies":["CAD"],"fields":["averages"],"decimal_places":5,"date":"2014-05-06"}},"base_currency":"USD"}', callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
// Act
client.getRates('USD', callback);
});
// Assert
it('sends the base currency as path', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/rates/USD.json');
});
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes a hash of rates data back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
"quotes": {
"CAD": {
"date": "2014-05-05T21:00:00+0000",
"bid": "1.09681",
"ask": "1.09702"
}
},
"meta": {
"skipped_currencies": [],
"request_time": "2014-05-06T18:27:56+0000",
"effective_params": {
"quote_currencies": [
"CAD"
],
"fields": [
"averages"
],
"decimal_places": 5,
"date": "2014-05-06"
}
},
"base_currency": "USD"
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"quotes":{"CAD":{"date":"2014-05-05T21:00:00+0000","bid":"1.09681","ask":"1.09702"}},"meta":{"skipped_currencies":[],"request_time":"2014-05-06T18:27:56+0000","effective_params":{"quote_currencies":["CAD"],"fields":["averages"],"decimal_places":5,"date":"2014-05-06"}},"base_currency":"USD"}');
});
it('passes the number of remaining quote any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
describe('when optional parameters are passed', function() {
var callback = sinon.spy();
before(function() {
// Arrange
sinon.stub(https, 'get').yields(
_createResponse(200, '{"quotes":{"GBP":{"midpoint":"0.60365","low_bid":"0.60360","low_ask":"0.60369","high_bid":"0.60360","high_ask":"0.60369","date":"2014-01-01T21:00:00+0000","bid":"0.60360","ask":"0.60369"},"EUR":{"midpoint":"0.72527","low_bid":"0.72522","low_ask":"0.72533","high_bid":"0.72522","high_ask":"0.72533","date":"2014-01-01T21:00:00+0000","bid":"0.72522","ask":"0.72533"}},"meta":{"skipped_currencies":[],"request_time":"2014-05-06T18:50:57+0000","effective_params":{"quote_currencies":["EUR","GBP"],"fields":["averages","highs","lows","midpoint"],"decimal_places":5,"date":"2014-01-01"}},"base_currency":"USD"}', callback)
);
var client = new OANDAExchangeRates({
api_key: '42'
});
var params = {
base: 'USD',
quote: ['EUR', 'GBP'],
fields: 'all',
date: '2014-01-01'
}
// Act
client.getRates(params, callback);
});
// Assert
it('sends the base currency and query string as path', function() {
https.get.firstCall.args[0].should.have.property('path', '/rates/api/v1/rates/USD.json?quote=EUR"e=GBP&fields=all&date=2014-01-01');
});
it('calls our callback', function() {
callback.callCount.should.equal(1);
});
it('passes a success flag back to our callback', function() {
callback.firstCall.args[0].should.have.property('success', true);
});
it('passes the HTTP status code back to our callback', function() {
callback.firstCall.args[0].should.have.property('statusCode', 200);
});
it('passes a hash of rates back to our callback', function() {
callback.firstCall.args[0].data.should.be.eql({
"quotes": {
"GBP": {
"midpoint": "0.60365",
"low_bid": "0.60360",
"low_ask": "0.60369",
"high_bid": "0.60360",
"high_ask": "0.60369",
"date": "2014-01-01T21:00:00+0000",
"bid": "0.60360",
"ask": "0.60369"
},
"EUR": {
"midpoint": "0.72527",
"low_bid": "0.72522",
"low_ask": "0.72533",
"high_bid": "0.72522",
"high_ask": "0.72533",
"date": "2014-01-01T21:00:00+0000",
"bid": "0.72522",
"ask": "0.72533"
}
},
"meta": {
"skipped_currencies": [],
"request_time": "2014-05-06T18:50:57+0000",
"effective_params": {
"quote_currencies": [
"EUR",
"GBP"
],
"fields": [
"averages",
"highs",
"lows",
"midpoint"
],
"decimal_places": 5,
"date": "2014-01-01"
}
},
"base_currency": "USD"
});
});
it('passes the raw serialized JSON back to our callback', function() {
callback.firstCall.args[0].should.have.property('raw', '{"quotes":{"GBP":{"midpoint":"0.60365","low_bid":"0.60360","low_ask":"0.60369","high_bid":"0.60360","high_ask":"0.60369","date":"2014-01-01T21:00:00+0000","bid":"0.60360","ask":"0.60369"},"EUR":{"midpoint":"0.72527","low_bid":"0.72522","low_ask":"0.72533","high_bid":"0.72522","high_ask":"0.72533","date":"2014-01-01T21:00:00+0000","bid":"0.72522","ask":"0.72533"}},"meta":{"skipped_currencies":[],"request_time":"2014-05-06T18:50:57+0000","effective_params":{"quote_currencies":["EUR","GBP"],"fields":["averages","highs","lows","midpoint"],"decimal_places":5,"date":"2014-01-01"}},"base_currency":"USD"}');
});
it('passes the number of remaining quote any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error code back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorCode');
});
it('does not pass any error message back to our callback', function() {
callback.firstCall.args[0].should.not.have.property('errorMessage');
});
// Restore
after(function() {
https.get.restore();
});
});
});
});