heroku-client
Version:
A wrapper for the Heroku v3 API
230 lines (190 loc) • 7.6 kB
JavaScript
var https = require("https"),
encryptor = require('../../lib/encryptor');
Request = require('../../lib/request'),
memjs = require('memjs'),
MockCache = require('../helpers/mockCache'),
MockRequest = require('../helpers/mockRequest'),
MockResponse = require('../helpers/mockResponse');
describe('request', function() {
it('uses the v3 API', function(done) {
makeRequest('/apps', {}, function() {
expect(https.request.mostRecentCall.args[0].headers['Accept']).toEqual('application/vnd.heroku+json; version=3');
done();
});
});
it('makes a request to a given path', function(done) {
makeRequest('/apps', {}, function() {
expect(https.request.mostRecentCall.args[0].path).toEqual("/apps");
done();
});
});
it('accepts a timeout', function(done) {
makeRequest('/apps', { timeout: 1 }, function(err, body) {
expect(err.message).toEqual('Request took longer than 1ms to complete.');
done();
}, { timeout: 10 });
});
it('writes the request body as a string', function(done) {
spyOn(MockRequest.prototype, 'write');
makeRequest('/apps', { body: { foo: 'bar' } }, function() {
expect(MockRequest.prototype.write).toHaveBeenCalledWith(JSON.stringify({ foo: 'bar' }));
done();
});
});
it('sets the Content-length when a body is present', function(done) {
spyOn(MockRequest.prototype, 'setHeader');
makeRequest('/apps', { body: { foo: 'bar' } }, function() {
expect(MockRequest.prototype.setHeader).toHaveBeenCalledWith('Content-length', JSON.stringify({ foo: 'bar' }).length);
done();
});
});
describe('callbacks and promises', function() {
it('sends a successful response to the callback', function(done) {
makeRequest('/apps', {}, function(err, body) {
expect(body).toEqual(JSON.parse('{ "message": "ok" }'));
done()
});
});
it('sends an error to the callback', function(done) {
makeRequest('/apps', {}, function(err, body) {
expect(err.message).toEqual('Expected response to be successful, got 404');
done()
}, { response: { statusCode: 404 } });
});
it('resolves a promise when successful', function(done) {
makeRequest('/apps', {}).then(function(body) {
expect(body).toEqual({ "message": "ok" });
done()
});
});
it('rejects a promise when there is an error', function(done) {
makeRequest('/apps', {}, null, { response: { statusCode: 404 } }).fail(function(err) {
expect(err.message).toEqual('Expected response to be successful, got 404');
done()
});
});
});
describe('options', function() {
it('uses an auth string', function(done) {
makeRequest('/apps', { token: 'api-token' }, function() {
expect(https.request.mostRecentCall.args[0].auth).toEqual(':api-token');
done();
});
});
it('GETs by default', function(done) {
makeRequest('/apps', {}, function() {
expect(https.request.mostRecentCall.args[0].method).toEqual('GET');
done();
});
});
it('accepts a method', function(done) {
makeRequest('/apps', { method: 'POST' }, function() {
expect(https.request.mostRecentCall.args[0].method).toEqual('POST');
done();
});
});
it('extends the default headers with custom headers', function(done) {
var expectedHeaders = {
'Arbitrary': 'header',
'Accept': 'application/vnd.heroku+json; version=3',
'Content-type': 'application/json',
'Range': 'id ]..; max=1000'
}
makeRequest('/apps', { headers: { 'Arbitrary': 'header' } }, function() {
expect(https.request.mostRecentCall.args[0].headers).toEqual(expectedHeaders);
done();
});
});
});
describe('status codes', function() {
it('expects a 2xx response by default', function(done) {
makeRequest('/apps', {}, function(err) {
expect(err.message).toEqual('Expected response to be successful, got 404');
done();
}, { response: { statusCode: 404 } })
});
});
describe('handling Range headers', function() {
it('sends a default Range header', function() {
makeRequest('/apps', {}, function (err, body) {
expect(https.request.mostRecentCall.args[0].headers['Range']).toEqual('id ]..; max=1000');
});
});
describe('when receiving a Next-Range header', function() {
it('sends the Next-Range header on the next request', function(done) {
makeRequest('/apps', {}, function (err, body) {
expect(https.request.mostRecentCall.args[0].headers['Range']).toEqual('id abcdefg..; max=1000');
done();
}, { response: { headers: { 'next-range': 'id abcdefg..; max=1000' } } });
});
it('aggregates response bodies', function(done) {
makeRequest('/apps', {}, function (err, body) {
expect(body).toEqual([{ message: 'ok' }, { message: 'ok' }]);
done();
}, { returnArray: true, response: { headers: { 'next-range': 'id abcdefg..; max=1000' } } });
});
});
});
describe('caching', function() {
var cache = new MockCache();
process.env.HEROKU_CLIENT_ENCRYPTION_SECRET = 'abcde';
beforeEach(function() {
spyOn(memjs.Client, 'create').andReturn(cache);
Request.connectCacheClient();
});
it('sends an etag from the cache', function(done) {
makeRequest('/apps', {}, function(err, body) {
expect(https.request.mostRecentCall.args[0].headers['If-None-Match']).toEqual('123');
done();
}, { response: { statusCode: 304 } });
});
it('gets with a postfix', function(done) {
spyOn(cache, 'get').andCallThrough();
makeRequest('/apps', { token: 'api-token' }, function(err, body) {
expect(cache.get).toHaveBeenCalledWith(encryptor.encrypt('/appsid ]..; max=1000api-token'), jasmine.any(Function));
done();
});
});
it('returns a cached body', function(done) {
makeRequest('/apps', {}, function(err, body) {
expect(body).toEqual({ cachedFoo: 'bar' });
done();
}, { response: { statusCode: 304 } });
});
it('writes to the cache when necessary', function(done) {
spyOn(cache, 'set');
makeRequest('/apps', { token: 'api-token' }, function(err, body) {
var expectedCache = JSON.stringify({
body: { message: 'ok' },
etag: '123'
});
expect(cache.set).toHaveBeenCalledWith(encryptor.encrypt('/appsid ]..; max=1000api-token'), encryptor.encrypt(expectedCache));
done();
}, { response: { headers: { etag: '123' } } });
});
});
});
function makeRequest(path, options, callback, testOptions) {
testOptions || (testOptions = {});
options.path = path;
spyOn(https, 'request').andCallFake(function (options, httpsCallback) {
if (options.headers.Range !== 'id ]..; max=1000') {
testOptions.response.headers['next-range'] = undefined;
}
var req = new MockRequest(),
res = new MockResponse(testOptions.response || {});
httpsCallback(res);
setTimeout(function () {
if (testOptions.returnArray) {
res.emit('data', '[{ "message": "ok" }]');
} else {
res.emit('data', '{ "message": "ok" }');
}
if (!req.isAborted) res.emit('end');
}, testOptions.timeout || 0);
return req;
});
return Request.request(options, function (err, body) {
if (callback) callback(err, body);
});
};