Browse Source

Drop standard in favour of xo

pull/15/head
Luke Childs 8 years ago
parent
commit
c44e8b013f
  1. 4
      package.json
  2. 235
      src/index.js
  3. 19
      test/base-url.js
  4. 19
      test/baseUrl.js
  5. 180
      test/cache.js
  6. 60
      test/endpoints.js
  7. 78
      test/errors.js
  8. 40
      test/queries.js
  9. 40
      test/types.js

4
package.json

@ -5,7 +5,7 @@
"main": "src/index.js",
"scripts": {
"test": "nyc ava",
"lint": "snazzy",
"lint": "xo",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"repository": {
@ -38,6 +38,6 @@
"delay": "^1.3.1",
"nock": "^9.0.2",
"nyc": "^10.0.0",
"snazzy": "^5.0.0"
"xo": "^0.17.1"
}
}

235
src/index.js

@ -1,124 +1,121 @@
const got = require('got')
const cacheManager = require('cache-manager')
const querystring = require('querystring')
const pkg = require('../package.json')
const expired = require('expired')
const deepAssign = require('deep-assign')
const querystring = require('querystring');
const got = require('got');
const cacheManager = require('cache-manager');
const expired = require('expired');
const deepAssign = require('deep-assign');
const pkg = require('../package.json');
class Onionoo {
// Constructor returns a new object so instance properties are private
constructor (options = {}) {
// Set default options
this.options = Object.assign({}, {
baseUrl: 'https://onionoo.torproject.org',
endpoints: [
'summary',
'details',
'bandwidth',
'weights',
'clients',
'uptime'
]
}, options)
if (options.cache !== false) {
this.options.cache = cacheManager.caching(Object.assign({}, {
store: 'memory',
max: 500
}, options.cache))
}
// Return object containing endpoint methods
return this.options.endpoints.reduce((onionoo, endpoint) => {
onionoo[endpoint] = this.createEndpointMethod(endpoint)
return onionoo
}, {})
}
// Returns a function to make requests to a given endpoint
createEndpointMethod (endpoint) {
return options => {
// Build query string (don't encode ':' for search filters)
const qs = querystring.encode(options).replace(/%3A/g, ':')
// Build url
let url = `${this.options.baseUrl}/${endpoint}`
url += qs ? `?${qs}` : ''
// If caching is enabled, check for url in cache
if (this.options.cache) {
return this.options.cache.get(url)
.then(cachedResult => {
let options = {}
// If we have it cached
if (cachedResult) {
// Return the cached entry if it's still valid
if (!expired(cachedResult.headers)) {
return cachedResult
// If it's stale, add last-modified date to headers
} else if (cachedResult.headers['last-modified']) {
options.headers = {
'if-modified-since': cachedResult.headers['last-modified']
}
}
}
// Make a request
return this.makeRequest(url, options)
.then(response => {
// If we get a 304, fill in the body
if (response.statusCode === 304) {
response.body = cachedResult.body
}
// If we get a 200 or 304, cache it
if ([200, 304].includes(response.statusCode)) {
this.options.cache.set(url, response)
}
return response
})
})
// If caching is disabled, just make the request
} else {
return this.makeRequest(url)
}
}
}
// Returns a promise for a request
makeRequest (url, options = {}) {
options = deepAssign({
headers: {
'user-agent': `onionoo-node-client v${pkg.version} (${pkg.homepage})`
}
}, options)
return got(url, options)
.catch(error => {
// Don't throw 304 responses
if (error.statusCode === 304) {
return error.response
} else {
throw error
}
})
.then(response => {
// Format response
response = {
statusCode: response.statusCode,
statusMessage: response.statusMessage,
headers: response.headers,
body: response.body && JSON.parse(response.body)
}
return response
})
}
// Constructor returns a new object so instance properties are private
constructor(options = {}) {
// Set default options
this.options = Object.assign({}, {
baseUrl: 'https://onionoo.torproject.org',
endpoints: [
'summary',
'details',
'bandwidth',
'weights',
'clients',
'uptime'
]
}, options);
if (options.cache !== false) {
this.options.cache = cacheManager.caching(Object.assign({}, {
store: 'memory',
max: 500
}, options.cache));
}
// Return object containing endpoint methods
return this.options.endpoints.reduce((onionoo, endpoint) => {
onionoo[endpoint] = this.createEndpointMethod(endpoint);
return onionoo;
}, {});
}
// Returns a function to make requests to a given endpoint
createEndpointMethod(endpoint) {
return options => {
// Build query string (don't encode ':' for search filters)
const qs = querystring.encode(options).replace(/%3A/g, ':');
// Build url
let url = `${this.options.baseUrl}/${endpoint}`;
url += qs ? `?${qs}` : '';
// If caching is enabled, check for url in cache
if (this.options.cache) {
return this.options.cache.get(url)
.then(cachedResult => {
let options = {};
// If we have it cached
if (cachedResult) {
// Return the cached entry if it's still valid
if (!expired(cachedResult.headers)) {
return cachedResult;
// If it's stale, add last-modified date to headers
} else if (cachedResult.headers['last-modified']) {
options.headers = {
'if-modified-since': cachedResult.headers['last-modified']
};
}
}
// Make a request
return this.makeRequest(url, options)
.then(response => {
// If we get a 304, fill in the body
if (response.statusCode === 304) {
response.body = cachedResult.body;
}
// If we get a 200 or 304, cache it
if ([200, 304].includes(response.statusCode)) {
this.options.cache.set(url, response);
}
return response;
});
});
}
// If caching is disabled, just make the request
return this.makeRequest(url);
};
}
// Returns a promise for a request
makeRequest(url, options = {}) {
options = deepAssign({
headers: {
'user-agent': `onionoo-node-client v${pkg.version} (${pkg.homepage})`
}
}, options);
return got(url, options)
.catch(err => {
// Don't throw 304 responses
if (err.statusCode === 304) {
return err.response;
}
throw err;
})
.then(response => {
// Format response
response = {
statusCode: response.statusCode,
statusMessage: response.statusMessage,
headers: response.headers,
body: response.body && JSON.parse(response.body)
};
return response;
});
}
}
module.exports = Onionoo
module.exports = Onionoo;

19
test/base-url.js

@ -0,0 +1,19 @@
import test from 'ava';
import nock from 'nock';
import Onionoo from '../';
import data from './fixtures/data';
test('Can pass in custom endpoint', async t => {
const baseUrl = 'http://foo.com';
const defaultEndpoint = data.defaultEndpoints[0];
const onionoo = new Onionoo({baseUrl});
const scope = nock(baseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse);
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
});

19
test/baseUrl.js

@ -1,19 +0,0 @@
import test from 'ava'
import nock from 'nock'
import data from './fixtures/data'
import Onionoo from '../'
test('Can pass in custom endpoint', async t => {
const baseUrl = 'http://foo.com'
const defaultEndpoint = data.defaultEndpoints[0]
const onionoo = new Onionoo({ baseUrl })
const scope = nock(baseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse)
const response = await onionoo[defaultEndpoint]()
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
})

180
test/cache.js

@ -1,133 +1,133 @@
import test from 'ava'
import nock from 'nock'
import subSeconds from 'date-fns/sub_seconds'
import delay from 'delay'
import data from './fixtures/data'
import Onionoo from '../'
import test from 'ava';
import nock from 'nock';
import subSeconds from 'date-fns/sub_seconds';
import delay from 'delay';
import Onionoo from '../';
import data from './fixtures/data';
test('Cache can be disabled', async t => {
const onionoo = new Onionoo({ cache: false })
const onionoo = new Onionoo({cache: false});
const defaultEndpoint = data.defaultEndpoints[0]
const responseHeaders = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=300'
}
const defaultEndpoint = data.defaultEndpoints[0];
const responseHeaders = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=300'
};
const scope = nock(data.defaultBaseUrl)
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response = await onionoo[defaultEndpoint]()
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
const scope2 = nock(data.defaultBaseUrl)
const scope2 = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response2 = await onionoo[defaultEndpoint]()
const response2 = await onionoo[defaultEndpoint]();
t.deepEqual(response2.body, data.dummyResponse)
t.truthy(scope2.isDone())
})
t.deepEqual(response2.body, data.dummyResponse);
t.truthy(scope2.isDone());
});
test('Responses with future max-age are cached', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const responseHeaders = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=300'
}
const defaultEndpoint = data.defaultEndpoints[0];
const responseHeaders = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=300'
};
const scope = nock(data.defaultBaseUrl)
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response = await onionoo[defaultEndpoint]()
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
const cachedResponse = await onionoo[defaultEndpoint]()
const cachedResponse = await onionoo[defaultEndpoint]();
t.deepEqual(cachedResponse.body, data.dummyResponse)
})
t.deepEqual(cachedResponse.body, data.dummyResponse);
});
test('Responses older than max-age are not cached', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const responseHeaders = {
date: subSeconds(new Date(), 15).toUTCString(),
age: 0,
'cache-control': 'public, max-age=10'
}
const defaultEndpoint = data.defaultEndpoints[0];
const responseHeaders = {
date: subSeconds(new Date(), 15).toUTCString(),
age: 0,
'cache-control': 'public, max-age=10'
};
const scope = nock(data.defaultBaseUrl)
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response = await onionoo[defaultEndpoint]()
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
const scope2 = nock(data.defaultBaseUrl)
const scope2 = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response2 = await onionoo[defaultEndpoint]()
const response2 = await onionoo[defaultEndpoint]();
t.deepEqual(response2.body, data.dummyResponse)
t.truthy(scope2.isDone())
})
t.deepEqual(response2.body, data.dummyResponse);
t.truthy(scope2.isDone());
});
test('When expired, add last-modified date to headers and handle 304', async t => {
const onionoo = new Onionoo()
const defaultEndpoint = data.defaultEndpoints[0]
const initialDate = new Date().toUTCString()
const responseHeaders = {
date: initialDate,
age: 0,
'cache-control': 'public, max-age=1',
'last-modified': initialDate
}
const scope = nock(data.defaultBaseUrl)
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0];
const initialDate = new Date().toUTCString();
const responseHeaders = {
date: initialDate,
age: 0,
'cache-control': 'public, max-age=1',
'last-modified': initialDate
};
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse, responseHeaders)
.reply(200, data.dummyResponse, responseHeaders);
const response = await onionoo[defaultEndpoint]()
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
const requestHeaders = {
'if-modified-since': initialDate
}
const responseHeaders304 = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=10',
'last-modified': initialDate
}
const requestHeaders = {
'if-modified-since': initialDate
};
const responseHeaders304 = {
date: new Date().toUTCString(),
age: 0,
'cache-control': 'public, max-age=10',
'last-modified': initialDate
};
const scope2 = nock(data.defaultBaseUrl, { requestHeaders })
const scope2 = nock(data.defaultBaseUrl, {requestHeaders})
.get(`/${defaultEndpoint}`)
.reply(304, null, responseHeaders304)
.reply(304, null, responseHeaders304);
const response2 = await delay(2000).then(onionoo[defaultEndpoint])
const response2 = await delay(2000).then(onionoo[defaultEndpoint]);
t.deepEqual(response2.body, data.dummyResponse)
t.truthy(scope2.isDone())
t.deepEqual(response2.body, data.dummyResponse);
t.truthy(scope2.isDone());
const cachedResponse = await onionoo[defaultEndpoint]()
const cachedResponse = await onionoo[defaultEndpoint]();
t.deepEqual(cachedResponse.body, data.dummyResponse)
})
t.deepEqual(cachedResponse.body, data.dummyResponse);
});

60
test/endpoints.js

@ -1,48 +1,48 @@
import test from 'ava'
import nock from 'nock'
import data from './fixtures/data'
import Onionoo from '../'
import test from 'ava';
import nock from 'nock';
import Onionoo from '../';
import data from './fixtures/data';
test('Onionoo instance contains default endpoints', t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
t.deepEqual(Object.keys(onionoo), data.defaultEndpoints)
})
t.deepEqual(Object.keys(onionoo), data.defaultEndpoints);
});
test('Default endpoint makes correct request', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const scope = nock(data.defaultBaseUrl)
const defaultEndpoint = data.defaultEndpoints[0];
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(200, data.dummyResponse)
.reply(200, data.dummyResponse);
const response = await onionoo[defaultEndpoint]()
const response = await onionoo[defaultEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
})
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
});
test('Can pass in custom endpoint array', t => {
const endpoints = [
'foo',
'bar'
]
const onionoo = new Onionoo({ endpoints })
const endpoints = [
'foo',
'bar'
];
const onionoo = new Onionoo({endpoints});
t.deepEqual(Object.keys(onionoo), endpoints)
})
t.deepEqual(Object.keys(onionoo), endpoints);
});
test('Custom endpoint makes correct request', async t => {
const customEndpoint = 'foo'
const onionoo = new Onionoo({ endpoints: [customEndpoint] })
const customEndpoint = 'foo';
const onionoo = new Onionoo({endpoints: [customEndpoint]});
const scope = nock(data.defaultBaseUrl)
const scope = nock(data.defaultBaseUrl)
.get(`/${customEndpoint}`)
.reply(200, data.dummyResponse)
.reply(200, data.dummyResponse);
const response = await onionoo[customEndpoint]()
const response = await onionoo[customEndpoint]();
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
})
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
});

78
test/errors.js

@ -1,48 +1,48 @@
import test from 'ava'
import nock from 'nock'
import data from './fixtures/data'
import Onionoo from '../'
import test from 'ava';
import nock from 'nock';
import Onionoo from '../';
import data from './fixtures/data';
test('Handle HTML responses for errors', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const scope = nock(data.defaultBaseUrl)
const defaultEndpoint = data.defaultEndpoints[0];
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(400, data.dummy400Response)
.reply(400, data.dummy400Response);
try {
await onionoo[defaultEndpoint]()
} catch (e) {
t.deepEqual(e.response.body, data.dummy400Response)
}
try {
await onionoo[defaultEndpoint]();
} catch (err) {
t.deepEqual(err.response.body, data.dummy400Response);
}
t.truthy(scope.isDone())
})
t.truthy(scope.isDone());
});
test('Throw useful errors for HTTP response codes', async t => {
const onionoo = new Onionoo()
const defaultEndpoint = data.defaultEndpoints[0]
const responseCodes = {
400: 'Bad Request',
404: 'Not Found',
500: 'Internal Server Error',
503: 'Service Unavailable'
}
for (const responseCode in responseCodes) {
const scope = nock(data.defaultBaseUrl)
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0];
const responseCodes = {
400: 'Bad Request',
404: 'Not Found',
500: 'Internal Server Error',
503: 'Service Unavailable'
};
Object.keys(responseCodes).forEach(async responseCode => {
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}`)
.reply(responseCode)
try {
await onionoo[defaultEndpoint]()
} catch (e) {
t.is(e.message, `Response code ${responseCode} (${responseCodes[responseCode]})`)
t.is(e.statusCode, parseInt(responseCode, 10))
t.is(e.statusMessage, responseCodes[responseCode])
}
t.truthy(scope.isDone())
}
})
.reply(responseCode);
try {
await onionoo[defaultEndpoint]();
} catch (err) {
t.is(err.message, `Response code ${responseCode} (${responseCodes[responseCode]})`);
t.is(err.statusCode, parseInt(responseCode, 10));
t.is(err.statusMessage, responseCodes[responseCode]);
}
t.truthy(scope.isDone());
});
});

40
test/queries.js

@ -1,32 +1,32 @@
import test from 'ava'
import nock from 'nock'
import data from './fixtures/data'
import Onionoo from '../'
import test from 'ava';
import nock from 'nock';
import Onionoo from '../';
import data from './fixtures/data';
test('Query string is built correctly', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const scope = nock(data.defaultBaseUrl)
const defaultEndpoint = data.defaultEndpoints[0];
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}?foo=bar`)
.reply(200, data.dummyResponse)
.reply(200, data.dummyResponse);
const response = await onionoo[defaultEndpoint]({ foo: 'bar' })
const response = await onionoo[defaultEndpoint]({foo: 'bar'});
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
})
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
});
test('":" char isn\'t url encoded so filters work', async t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
const defaultEndpoint = data.defaultEndpoints[0]
const scope = nock(data.defaultBaseUrl)
const defaultEndpoint = data.defaultEndpoints[0];
const scope = nock(data.defaultBaseUrl)
.get(`/${defaultEndpoint}?foo=key:value`)
.reply(200, data.dummyResponse)
.reply(200, data.dummyResponse);
const response = await onionoo[defaultEndpoint]({ foo: 'key:value' })
const response = await onionoo[defaultEndpoint]({foo: 'key:value'});
t.deepEqual(response.body, data.dummyResponse)
t.truthy(scope.isDone())
})
t.deepEqual(response.body, data.dummyResponse);
t.truthy(scope.isDone());
});

40
test/types.js

@ -1,33 +1,33 @@
import test from 'ava'
import Onionoo from '../'
import test from 'ava';
import Onionoo from '../';
test('Onionoo is a function', t => {
t.is(typeof Onionoo, 'function')
})
t.is(typeof Onionoo, 'function');
});
test('Onionoo cannot be invoked without \'new\'', t => {
t.throws(() => Onionoo())
t.notThrows(() => new Onionoo())
})
t.throws(() => Onionoo()); // eslint-disable-line new-cap
t.notThrows(() => new Onionoo());
});
test('Onionoo instance is an object', t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
t.is(typeof onionoo, 'object')
})
t.is(typeof onionoo, 'object');
});
test('Onionoo instance contains endpoint methods', t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
Object.keys(onionoo).forEach(endpoint => {
t.is(typeof onionoo[endpoint], 'function')
})
})
Object.keys(onionoo).forEach(endpoint => {
t.is(typeof onionoo[endpoint], 'function');
});
});
test('Endpoint methods return promise', t => {
const onionoo = new Onionoo()
const onionoo = new Onionoo();
Object.keys(onionoo).forEach(endpoint => {
t.true(onionoo[endpoint]() instanceof Promise)
})
})
Object.keys(onionoo).forEach(endpoint => {
t.true(onionoo[endpoint]() instanceof Promise);
});
});

Loading…
Cancel
Save