Socket
Socket
Sign inDemoInstall

heroku-client

Package Overview
Dependencies
Maintainers
1
Versions
72
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

heroku-client - npm Package Compare versions

Comparing version 0.2.0 to 1.0.0

bin/docs

140

lib/heroku.js

@@ -1,111 +0,67 @@

var inflection = require('inflection'),
client = require('./request'),
resources = require('./resources').resources,
_ = require('underscore');
var Request = require('./request');
exports.request = client.request;
exports.Heroku = Heroku;
module.exports = Heroku;
function Heroku(options) {
this.request = function(_options, callback) {
if (typeof _options === 'function') {
callback = _options;
_options = options;
} else {
_options = _.extend(_.clone(options), _options);
}
return client.request(_options, function(err, body) {
if (callback) callback(err, body);
});
};
function Heroku (options) {
this.options = options;
}
_.each(resources, function (resource) {
buildResource(resource);
});
Heroku.configure = function configure (config) {
if (config.cache && !process.env.HEROKU_CLIENT_ENCRYPTION_SECRET) {
console.error('Must supply HEROKU_CLIENT_ENCRYPTION_SECRET in order to cache');
process.exit(1);
}
function buildResource (resource) {
_.each(resource.actions, function (action, actionName) {
buildAction(action, actionName);
});
}
if (config.cache) {
Request.connectCacheClient();
}
function buildAction (action, actionName) {
var constructor = getResource(action.path);
constructor.prototype[getName(actionName)] = function (body, callback) {
var requestPath = action.path,
callback;
this.params.forEach(function (param) {
requestPath = requestPath.replace(/{[a-z_]+}/, param);
});
var options = {
method: action.method,
path: requestPath,
expectedStatus: action.statuses
};
if (typeof arguments[0] === 'function') {
callback = body;
} else if (typeof arguments[0] === 'object') {
options = _.extend(options, { body: body });
}
return this.client.request(options, callback);
};
return this;
}
function getResource(path) {
var proxy = Heroku,
segments;
Heroku.request = Request.request;
path = path.split(/\//);
segments = path.slice(1, path.length);
Heroku.prototype.request = function request (options, callback) {
var key;
segments.forEach(function (segment) {
var constructor;
if (proxy.prototype && proxy.prototype[segment]) {
return proxy = proxy.prototype[segment]._constructor;
if (typeof options === 'function') {
callback = options;
options = this.options;
} else {
for (key in this.options) {
if (Object.keys(options).indexOf(key) == -1) options[key] = this.options[key];
}
}
if (!segment.match(/{.*}/)) {
constructor = function (client, params) {
this.client = client;
this.params = params;
};
return Request.request(options, function requestCallback (err, body) {
if (callback) callback(err, body);
});
};
proxy.prototype[segment] = function (param) {
var client, params, resource;
Heroku.prototype.get = function get (path, callback) {
return this.request({ method: 'GET', path: path }, callback);
};
if (this instanceof Heroku) {
client = this;
} else {
client = this.client;
}
Heroku.prototype.post = function post (path, body, callback) {
if (typeof body === 'function') {
callback = body;
body = {};
}
params = this.params || [];
if (param) params = params.concat(param);
return this.request({ method: 'POST', path: path, body: body }, callback);
};
return new constructor(client, params);
};
Heroku.prototype.patch = function patch (path, body, callback) {
if (typeof body === 'function') {
callback = body;
body = {};
}
proxy.prototype[segment]._constructor = constructor;
return this.request({ method: 'PATCH', path: path, body: body }, callback);
};
return proxy = constructor;
}
});
Heroku.prototype.delete = function _delete (path, callback) {
return this.request({ method: 'DELETE', path: path }, callback);
};
return proxy;
}
function getName(name) {
name = name.toLowerCase();
name = inflection.dasherize(name).replace(/-/g, '_');
name = inflection.camelize(name, true);
return name;
}
require('./resourceBuilder').build();

@@ -1,108 +0,189 @@

var https = require('https'),
memjs = require('memjs'),
q = require('q'),
_ = require('underscore'),
var https = require('https'),
agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }),
concat = require('concat-stream'),
encryptor = require('./encryptor'),
memjs = require('memjs'),
q = require('q'),
cache;
exports.request = function request(options, callback) {
options || (options = {});
var deferred = q.defer(),
path = options.path;
module.exports = Request;
getCache(path, options.cacheKeyPostfix, function(cachedResponse) {
var headers = _.extend({
'Accept': 'application/vnd.heroku+json; version=3',
'Content-type': 'application/json'
}, options.headers || {});
if (cachedResponse) {
headers['If-None-Match'] = cachedResponse.etag;
}
/*
* Create an object capable of making API
* calls. Accepts custom request options and
* a callback function.
*/
function Request (options, callback) {
this.options = options || {};
this.callback = callback;
this.deferred = q.defer();
this.nextRange = 'id ]..; max=1000';
}
var requestOptions = {
hostname: 'api.heroku.com',
port: 443,
path: path,
auth: ':' + options.token,
method: options.method || 'GET',
headers: headers
};
var req = https.request(requestOptions, function(res) {
if (res.statusCode === 304 && cachedResponse) {
deferred.resolve(cachedResponse.body);
callback(null, cachedResponse.body);
} else {
var buffer = '';
/*
* Instantiate a Request object and makes a
* request, returning the request promise.
*/
Request.request = function request (options, callback) {
var req = new Request(options, function (err, body) {
if (callback) callback(err, body);
});
res.on('data', function(data) {
buffer += data;
});
return req.request();
};
res.on('end', function() {
if (expectedStatus(res, options)) {
handleSuccess(res, buffer, options, deferred, callback);
} else {
handleFailure(res, buffer, options, deferred, callback);
}
});
}
});
if (options.body) {
var body = JSON.stringify(options.body);
/*
* Check for a cached response, then
* perform an API request. Return the
* request object's promise.
*/
Request.prototype.request = function request () {
this.getCache(this.performRequest.bind(this));
return this.deferred.promise;
};
req.setHeader('Content-length', body.length);
req.write(body);
}
req.on('error', function(err) {
deferred.reject(err);
callback(err);
});
/*
* Perform the actual API request.
*/
Request.prototype.performRequest = function performRequest (cachedResponse) {
var headers,
key,
requestOptions,
req;
if (options.timeout && options.timeout > 0) {
req.setTimeout(options.timeout, function() {
req.abort();
this.cachedResponse = cachedResponse;
var err = new Error('Request took longer than ' + options.timeout + 'ms to complete.');
deferred.reject(err);
callback(err);
});
}
headers = {
'Accept': 'application/vnd.heroku+json; version=3',
'Content-type': 'application/json',
'Range': this.nextRange
};
req.end();
});
this.options.headers || (this.options.headers = {});
for (key in this.options.headers) {
headers[key] = this.options.headers[key];
}
return deferred.promise;
}
if (this.cachedResponse) {
headers['If-None-Match'] = this.cachedResponse.etag;
}
function expectedStatus(res, options) {
if (options.expectedStatus) {
if (Array.isArray(options.expectedStatus)) {
return options.expectedStatus.indexOf(res.statusCode) > -1
requestOptions = {
agent: agent,
host: 'api.heroku.com',
port: 443,
path: this.options.path,
auth: ':' + this.options.token,
method: this.options.method || 'GET',
headers: headers
};
req = https.request(requestOptions, this.handleResponse.bind(this));
this.writeBody(req);
this.setRequestTimeout(req);
req.on('error', this.handleError.bind(this));
req.end();
};
/*
* Handle an API response, returning the
* cached body if it's still valid, or the
* new API response.
*/
Request.prototype.handleResponse = function handleResponse (res) {
var _this = this,
resReader = concat(directResponse);
if (res.statusCode === 304 && this.cachedResponse) {
if (this.cachedResponse.nextRange) {
this.nextRequest(this.cachedResponse.nextRange, this.cachedResponse.body);
} else {
return res.statusCode === options.expectedStatus;
this.updateAggregate(this.cachedResponse.body);
this.deferred.resolve(this.aggregate);
this.callback(null, this.aggregate);
}
} else {
return res.statusCode.toString().match(/^2\d{2}$/);
res.pipe(resReader);
}
function directResponse (data) {
if (res.statusCode.toString().match(/^2\d{2}$/)) {
_this.handleSuccess(res, data);
} else {
_this.handleFailure(res, data);
}
}
};
/*
* If the request options include a body,
* write the body to the request and set
* an appropriate 'Content-length' header.
*/
Request.prototype.writeBody = function writeBody (req) {
if (!this.options.body) return;
var body = JSON.stringify(this.options.body);
req.setHeader('Content-length', body.length);
req.write(body);
}
function handleFailure(res, buffer, options, deferred, callback) {
var statusString = options.expectedStatus,
message;
if (options.expectedStatus) {
if (Array.isArray(options.expectedStatus)) {
statusString = JSON.stringify(options.expectedStatus);
}
/*
* If the request options include a timeout,
* set the timeout and provide a callback
* function in case the request exceeds the
* timeout period.
*/
Request.prototype.setRequestTimeout = function setRequestTimeout (req) {
var _this = this;
message = 'Expected response ' + statusString + ', got ' + res.statusCode
} else {
message = 'Expected response to be successful, got ' + res.statusCode
}
if (!this.options.timeout) return;
var err = new Error(message);
req.setTimeout(this.options.timeout, function () {
var err = new Error('Request took longer than ' + _this.options.timeout + 'ms to complete.');
req.abort();
_this.deferred.reject(err);
_this.callback(err);
});
}
/*
* In the event of an error in performing
* the API request, reject the deferred
* object and return an error to the callback.
*/
Request.prototype.handleError = function handleError (err) {
this.deferred.reject(err);
this.callback(err);
}
/*
* In the event of a non-successful API request,
* fail with an appropriate error message and
* status code.
*/
Request.prototype.handleFailure = function handleFailure (res, buffer) {
var options = this.options,
callback = this.callback,
deferred = this.deferred,
message = 'Expected response to be successful, got ' + res.statusCode,
err;
err = new Error(message);
err.statusCode = res.statusCode;

@@ -115,39 +196,102 @@ err.body = JSON.parse(buffer || "{}");

function handleSuccess(res, buffer, options, deferred, callback) {
var body = JSON.parse(buffer || '{}');
setCache(options.path, options.cacheKeyPostfix, res, body);
/*
* In the event of a successful API response,
* write the response to the cache and resolve
* with the response body.
*/
Request.prototype.handleSuccess = function handleSuccess (res, buffer) {
var options = this.options,
callback = this.callback,
deferred = this.deferred,
body = JSON.parse(buffer || '{}');
deferred.resolve(body);
callback(null, body);
this.setCache(res, body);
if (res.headers['next-range']) {
this.nextRequest(res.headers['next-range'], body);
} else {
this.updateAggregate(body);
deferred.resolve(this.aggregate);
callback(null, this.aggregate);
}
}
function getCache(path, postfix, callback) {
/*
* Since this request isn't the full response (206 or
* 304 with a cached Next-Range), perform the next
* request for more data.
*/
Request.prototype.nextRequest = function nextRequest (nextRange, body) {
this.updateAggregate(body);
this.nextRange = nextRange;
this.request();
}
/*
* If the cache client is alive, get the
* cached response from the cache.
*/
Request.prototype.getCache = function getCache (callback) {
if (!cache) return callback(null);
var key = path + '-' + postfix;
var key = this.getCacheKey();
cache.get(key, function(err, res) {
cache.get(key, function (err, res) {
res = res ? encryptor.decrypt(res.toString()) : res;
callback(JSON.parse(res));
});
}
};
function setCache(path, cacheKeyPostfix, res, body) {
/*
* If the cache client is alive, write the
* provided response and body to the cache.
*/
Request.prototype.setCache = function setCache (res, body) {
if ((!cache) || !(res.headers.etag)) return;
var key = path + '-' + cacheKeyPostfix;
var key = this.getCacheKey();
var value = JSON.stringify({
body: body,
etag: res.headers.etag
etag: res.headers.etag,
nextRange: res.headers['next-range']
});
value = encryptor.encrypt(value);
cache.set(key, value);
}
exports.connectCacheClient = function connectCacheClient() {
cache = memjs.Client.create();
/*
* Returns a cache key comprising the request path,
* the 'Next Range' header, and the user's API token.
*/
Request.prototype.getCacheKey = function getCacheKey () {
return encryptor.encrypt(this.options.path + this.nextRange + this.options.token);
};
if (process.env.NODE_ENV === 'production') {
exports.connectCacheClient();
/*
* If given an object, sets aggregate to object,
* otherwise concats array onto aggregate.
*/
Request.prototype.updateAggregate = function updateAggregate (aggregate) {
if (aggregate instanceof Array) {
this.aggregate || (this.aggregate = []);
this.aggregate = this.aggregate.concat(aggregate);
} else {
this.aggregate = aggregate;
}
}
/*
* Connect a cache client.
*/
Request.connectCacheClient = function connectCacheClient() {
cache = memjs.Client.create();
};
{
"name": "heroku-client",
"version": "0.2.0",
"version": "1.0.0",
"description": "A wrapper for the Heroku v3 API",

@@ -11,3 +11,3 @@ "main": "./lib/heroku.js",

"type": "git",
"url": "https://github.com/jclem/node-heroku"
"url": "https://github.com/jclem/node-heroku-client"
},

@@ -20,3 +20,3 @@ "keywords": [

"bugs": {
"url": "https://github.com/jclem/node-heroku/issues"
"url": "https://github.com/jclem/node-heroku-client/issues"
},

@@ -29,5 +29,6 @@ "devDependencies": {

"memjs": "~0.6.0",
"underscore": "~1.5.1",
"inflection": "~1.2.6"
"inflection": "~1.2.6",
"concat-stream": "~1.1.0",
"path-proxy": "~1.0"
}
}

@@ -5,30 +5,105 @@ # heroku-client [![Build Status](https://travis-ci.org/jclem/node-heroku-client.png?branch=master)](https://travis-ci.org/jclem/node-heroku-client)

## Install
```sh
$ npm install heroku-client --save
```
## Documentation
Docs are auto-generated and live in the [docs directory](https://github.com/heroku/node-heroku-client/tree/development/docs).
## Usage
`heroku-client` works by providing functions that return proxy objects for
interacting with different resources through the Heroku API.
To begin, require the Heroku module and create a client, passing in an API
token:
```javascript
// Create a new client and give it an API token
var Heroku = require('heroku-client').Heroku;
heroku = new Heroku({ token: user.apiToken });
var Heroku = require('heroku-client'),
heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN });
```
The simplest example is listing a user's apps. First, we call `heroku.apps()`,
which returns a proxy object to the /apps endpoint, then we call `list()` to
actually perform the API call:
```javascript
heroku.apps().list(function (err, apps) {
console.log(apps);
// `apps` is a parsed JSON response from the API
});
```
heroku.apps('my-app').info(function (err, app) {
console.log(app);
The advantage of using proxy objects is that they are reusable. Let's get the
info for the user's app "my-app", get the dynos for the app, and
remove a collaborator:
```javascript
var app = heroku.apps('my-app');
app.info(function (err, app) {
// Details about the `app`
});
heroku.apps().create({ name: 'my-new-app' }, function (err, app) {
console.log(app);
app.dynos().list(function (err, dynos) {
// List of the app's `dynos`
});
var newPlan = { plan: { name: 'papertrail:fixa' } };
heroku.apps('my-app').addons('papertrail').update(newPlan, function (err, addon) {
console.log(addon);
app.collaborators('user@example.com').delete(function (err, collaborator) {
// The `collaborator` has been removed unless `err`
});
```
Requests that require a body are easy, as well. Let's add a collaborator to
the user's app "another-app":
```javascript
var app = heroku.apps('another-app'),
user = { email: 'new-user@example.com' };
app.collaborators().create({ user: user }, function (err, collaborator) {
// `collaborator` is the newly added collaborator unless `err`
});
```
### Generic Requests
heroku-client has `get`, `post`, `patch`, and `delete` functions which can make requests with the specified HTTP method to any endpoint:
```javascript
heroku.get('/apps', function (err, apps) {
});
// Request body is optional on both `post` and `patch`
heroku.post('/apps', function (err, app) {
});
heroku.post('/apps', { name: 'my-new-app' }, function (err, app) {
});
heroku.patch('/apps/my-app', { name: 'my-renamed-app' }, function (err, app) {
});
heroku.delete('/apps/my-old-app', function (err, app) {
});
```
There is also an even more generic `request` function that can accept many more options:
```javascript
heroku.request({
method: 'GET',
path: '/apps',
headers: {
'Foo': 'Bar'
}
}, function (err, responseBody) {
});
```
### Promises
heroku-client works with Node-style callbacks, but also implements promises with the [q][q] library.
heroku-client works with Node-style callbacks, but also implements promises with the [Q][q] library.

@@ -54,21 +129,46 @@ ```javascript

When `NODE_ENV` is set to "production", heroku-client will create a memcached client using [memjs][memjs]. See the memjs repo for configuration instructions.
heroku-client performs caching by creating a memcached client using [memjs][memjs]. See the memjs repo for environment-specific configuration instructions and details.
For local development with caching, it's enough to start a memcached server and set `MEMCACHIER_SERVERS` to `0.0.0.0:11211` in your `.env` file.
heroku-client will cache any response from the Heroku API that comes with an `ETag` header, and each response is cached individually (i.e. even though the client might make multiple calls for a user's apps and then aggregate them into a single JSON array, each required API call is individually cached). For each API request it performs, heroku-client sends an `If-None-Match` header if there is a cached response for the API request. If API returns a 304 response code, heroku-client returns the cached response. Otherwise, it writes the new API response to the cache and returns that.
You will also need to pass an option called `cacheKeyPostfix` when creating your heroku-client client:
To tell heroku-client to perform caching, call the `configure` function:
```javascript
var heroku = new Heroku({ token: user.apiToken, cacheKeyPostfix: user.id });
var Heroku = require('heroku').configure({ cache: true });
```
This ensures that API responses are cached and properly scoped to the user that heroku-client is making requests on behalf of.
This requires a `MEMCACHIER_SERVERS` environment variable, as well as a `HEROKU_CLIENT_ENCRYPTION_SECRET` environment variable that heroku-client uses to build cache keys and encrypt cache contents.
`HEROKU_CLIENT_ENCRYPTION_SECRET` should be a long, random string of characters. heroku-client includes [`bin/secret`][bin_secret] as one way of generating values for this variable. **Do not publish this secret or commit it to source control. If it's compromised, flush your memcache and generate a new encryption secret.**
`MEMCACHIER_SERVERS` can be a single `hostname:port` memache address, or a comma-separated list of memcache addresses, e.g. `example.com:11211,example.net:11211`. Note that while the environment variable that memjs looks for is [named for the MemCachier service it was originally built for][memcachier], it will work with any memcache server that speaks the binary protocol.
## Contributing
### Updating resources
To fetch the latest schema, generate documentation, and run the tests:
```sh
$ bin/update
```
Inspect your changes, and [bump the version number accordingly](http://semver.org/) when cutting a release.
### Generating documentation
Documentation for heroku-client is auto-generated from [the resources manifest](https://github.com/heroku/node-heroku-client/blob/development/lib/resources.js).
Docs are generated like so:
```bash
$ bin/docs
```
Generating docs also runs a cursory test, ensuring that every documented function *is* a function that can be called.
### Running tests
node-heroku uses [jasmine-node][jasmine-node] for tests:
heroku-client uses [jasmine-node][jasmine-node] for tests:
```javascript
```bash
$ npm test

@@ -80,2 +180,4 @@ ```

[memjs]: https://github.com/alevy/memjs
[bin_secret]: https://github.com/heroku/node-heroku-client/blob/development/bin/secret
[memcachier]: https://www.memcachier.com
[jasmine-node]: https://github.com/mhevery/jasmine-node
module.exports = MockCache;
var encryptor = require('../../lib/encryptor');
function MockCache() {

@@ -7,4 +9,7 @@ }

MockCache.prototype.get = function(key, callback) {
var body = { cachedFoo: "bar" };
callback(null, JSON.stringify({ etag: '123', body: body }));
var body = { cachedFoo: "bar" },
value = JSON.stringify({ etag: '123', body: body });
value = encryptor.encrypt(value);
callback(null, value);
};

@@ -11,0 +16,0 @@

@@ -1,2 +0,2 @@

var EventEmitter = require("events").EventEmitter;
var Stream = require('stream').Stream;

@@ -14,4 +14,4 @@ module.exports = MockResponse;

for (var key in EventEmitter.prototype) {
MockResponse.prototype[key] = EventEmitter.prototype[key];
for (var key in Stream.prototype) {
MockResponse.prototype[key] = Stream.prototype[key];
}

@@ -1,8 +0,8 @@

var client = require('../../lib/request'),
herokuModule = require('../../lib/heroku'),
heroku = new herokuModule.Heroku({ key: '12345' });
var Heroku = require('../../lib/heroku'),
Request = require('../../lib/request'),
heroku = new Heroku({ token: '12345' });
describe('Heroku', function() {
beforeEach(function() {
spyOn(client, 'request').andCallFake(function(options, callback) {
spyOn(Request, 'request').andCallFake(function(options, callback) {
callback();

@@ -12,13 +12,13 @@ });

it('passes its method into the request', function(done) {
heroku.apps('my-app').create({}, function() {
expect(client.request.mostRecentCall.args[0].method).toEqual('POST');
done();
it('passes its method into the request', function() {
heroku.apps().create({}, function() {
expect(Request.request.mostRecentCall.args[0].method).toEqual('POST');
});
});
it('passes its expected status into the request', function(done) {
heroku.apps('my-app').dynos().list(function() {
expect(client.request.mostRecentCall.args[0].expectedStatus).toEqual([200, 206]);
done();
describe('requests with the wrong number of parameters', function() {
it('throws an error', function() {
expect(function () {
heroku.apps('my-app').list();
}).toThrow(new Error('Invalid number of params in path (expected 0, got 1).'));
});

@@ -28,20 +28,17 @@ });

describe('requests with no body', function() {
it('can perform a request with no parameters', function(done) {
it('can perform a request with no parameters', function() {
heroku.apps().list(function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps');
});
});
it('can perform a request with one parameter', function(done) {
it('can perform a request with one parameter', function() {
heroku.apps('my-app').info(function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps/my-app');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps/my-app');
});
});
it('can perform a request with multiple parameters', function(done) {
it('can perform a request with multiple parameters', function() {
heroku.apps('my-app').collaborators('jonathan@heroku.com').info(function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/collaborators/jonathan@heroku.com');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/collaborators/jonathan@heroku.com');
});

@@ -52,13 +49,11 @@ });

describe('requests with a body and no parameters', function() {
it('requests the correct path', function(done) {
it('requests the correct path', function() {
heroku.apps().create({ name: 'my-app' }, function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps');
});
});
it('sends the request body', function(done) {
it('sends the request body', function() {
heroku.apps().create({ name: 'my-new-app' }, function() {
expect(client.request.mostRecentCall.args[0].body).toEqual({ name: 'my-new-app' });
done();
expect(Request.request.mostRecentCall.args[0].body).toEqual({ name: 'my-new-app' });
});

@@ -69,13 +64,11 @@ });

describe('requests with a body and one parameter', function() {
it('requests the correct path', function(done) {
it('requests the correct path', function() {
heroku.apps('my-app').addons().create({ name: 'papertrail:choklad' }, function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/addons');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/addons');
});
});
it('sends the request body', function(done) {
it('sends the request body', function() {
heroku.apps('my-app').addons().create({ name: 'papertrail:choklad' }, function() {
expect(client.request.mostRecentCall.args[0].body).toEqual({ name: 'papertrail:choklad' });
done();
expect(Request.request.mostRecentCall.args[0].body).toEqual({ name: 'papertrail:choklad' });
});

@@ -86,16 +79,86 @@ });

describe('requests with a body and multiple parameters', function() {
it('requests the correct path', function(done) {
it('requests the correct path', function() {
heroku.apps('my-app').addons('papertrail:choklad').update({ name: 'papertrail:fixa' }, function() {
expect(client.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/addons/papertrail:choklad');
done();
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps/my-app/addons/papertrail:choklad');
});
});
it('sends the request body', function(done) {
it('sends the request body', function() {
heroku.apps('my-app').addons('papertrail:choklad').update({ name: 'papertrail:fixa' }, function() {
expect(client.request.mostRecentCall.args[0].body).toEqual({ name: 'papertrail:fixa' });
done();
expect(Request.request.mostRecentCall.args[0].body).toEqual({ name: 'papertrail:fixa' });
});
});
});
describe('#get', function() {
it('does a GET request', function() {
heroku.get('/apps', function () {
expect(Request.request.mostRecentCall.args[0].method).toEqual('GET');
});
});
it('requests the specified path', function() {
heroku.get('/apps', function () {
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps');
});
});
});
describe('#post', function() {
it('does a POST request', function() {
heroku.post('/apps', function () {
expect(Request.request.mostRecentCall.args[0].method).toEqual('POST');
});
});
it('requests the specified path', function() {
heroku.post('/apps', function () {
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps');
});
});
describe('when a body is supplied', function() {
it('sends the request body', function() {
heroku.post('/apps', { name: 'my-app' }, function () {
expect(Request.request.mostRecentCall.args[0].body).toEqual({ name: 'my-app' });
});
});
});
});
describe('#patch', function() {
it('does a PATCH request', function() {
heroku.patch('/apps', function () {
expect(Request.request.mostRecentCall.args[0].method).toEqual('PATCH');
});
});
it('requests the specified path', function() {
heroku.patch('/apps', function () {
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps');
});
});
describe('when a body is supplied', function() {
it('sends the request body', function() {
heroku.patch('/apps', { name: 'my-app' }, function () {
expect(Request.request.mostRecentCall.args[0].body).toEqual({ name: 'my-app' });
});
});
});
});
describe('#delete', function() {
it('does a DELETE request', function() {
heroku.delete('/apps/my-app', function () {
expect(Request.request.mostRecentCall.args[0].method).toEqual('DELETE');
});
});
it('requests the specified path', function() {
heroku.delete('/apps/my-app', function () {
expect(Request.request.mostRecentCall.args[0].path).toEqual('/apps/my-app');
});
});
});
});
var https = require("https"),
client = require('../../lib/request'),
encryptor = require('../../lib/encryptor');
Request = require('../../lib/request'),
memjs = require('memjs'),

@@ -104,3 +105,4 @@ MockCache = require('../helpers/mockCache'),

'Accept': 'application/vnd.heroku+json; version=3',
'Content-type': 'application/json'
'Content-type': 'application/json',
'Range': 'id ]..; max=1000'
}

@@ -116,3 +118,3 @@

describe('status codes', function() {
it('expects a 200 response by default', function(done) {
it('expects a 2xx response by default', function(done) {
makeRequest('/apps', {}, function(err) {

@@ -123,30 +125,26 @@ expect(err.message).toEqual('Expected response to be successful, got 404');

});
});
it('accepts a single expected status code', function(done) {
makeRequest('/apps', { expectedStatus: 201 }, function(err, body) {
expect(err.message).toEqual('Expected response 201, got 200');
done();
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');
});
});
it('accepts an array of expected status codes (failure)', function(done) {
makeRequest('/apps', { expectedStatus: [201, 204] }, function(err, body) {
expect(err.message).toEqual('Expected response [201,204], got 200');
done();
}, { response: { statusCode: 200 } });
});
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('accepts an array of expected status codes (success, first)', function(done) {
makeRequest('/apps', { expectedStatus: [201, 204] }, function(err, body) {
expect(err).toEqual(null);
done();
}, { response: { statusCode: 201 } });
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' } } });
});
});
it('accepts an array of expected status codes (success, last)', function(done) {
makeRequest('/apps', { expectedStatus: [201, 204] }, function(err, body) {
expect(err).toEqual(null);
done();
}, { response: { statusCode: 204 } });
});
});

@@ -157,5 +155,7 @@

process.env.HEROKU_CLIENT_ENCRYPTION_SECRET = 'abcde';
beforeEach(function() {
spyOn(memjs.Client, 'create').andReturn(cache);
client.connectCacheClient();
Request.connectCacheClient();
});

@@ -173,4 +173,4 @@

makeRequest('/apps', { cacheKeyPostfix: '123' }, function(err, body) {
expect(cache.get).toHaveBeenCalledWith('/apps-123', jasmine.any(Function));
makeRequest('/apps', { token: 'api-token' }, function(err, body) {
expect(cache.get).toHaveBeenCalledWith(encryptor.encrypt('/appsid ]..; max=1000api-token'), jasmine.any(Function));
done();

@@ -190,3 +190,3 @@ });

makeRequest('/apps', { cacheKeyPostfix: '123' }, function(err, body) {
makeRequest('/apps', { token: 'api-token' }, function(err, body) {
var expectedCache = JSON.stringify({

@@ -197,3 +197,3 @@ body: { message: 'ok' },

expect(cache.set).toHaveBeenCalledWith('/apps-123', expectedCache);
expect(cache.set).toHaveBeenCalledWith(encryptor.encrypt('/appsid ]..; max=1000api-token'), encryptor.encrypt(expectedCache));
done();

@@ -209,10 +209,19 @@ }, { response: { headers: { etag: '123' } } });

spyOn(https, 'request').andCallFake(function(options, httpsCallback) {
var req = new MockRequest();
var res = new MockResponse(testOptions.response || {});
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() {
res.emit('data', '{ "message": "ok" }');
setTimeout(function () {
if (testOptions.returnArray) {
res.emit('data', '[{ "message": "ok" }]');
} else {
res.emit('data', '{ "message": "ok" }');
}
if (!req.isAborted) res.emit('end');

@@ -225,5 +234,5 @@ }, testOptions.timeout || 0);

return client.request(options, function(err, body) {
return Request.request(options, function (err, body) {
if (callback) callback(err, body);
});
};

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc