Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Rewrite to adopt node conventions #1

Merged
merged 10 commits into from about 2 years ago

3 participants

Will White Nathan Vander Wilt Mikeal Rogers
Will White
Collaborator

Hi @natevm,
I've taken a first stab at rewriting this module with the goal of making it's conventions more familiar to a node developer.

Summary of the changes:

  • Use the battle-tested Request module for HTTPS requests.
  • Callbacks accept err as their first argument, so connection level errors are reported properly.
  • Added mocha tests

Let me know if you have any feedback or opinions about this becoming a 0.2.x release of the project.

Thanks,
Will

Nathan Vander Wilt
Owner
natevw commented

Uh-oh, this makes me feel bad!

These changes look very good overall, but I realize now I forgot to put any note in the documentation about what became of this library.

Basically, this library was sort of the "version 0.1" of Fermata — which now has a Chargify plugin that I recommend instead of this standalone version. There's nothing wrong per se with this library (especially with these improvements) but it makes more sense to me to support just one "REST wrapping" library that's pluggable for config-type stuff but shares a common JavaScript-side API across every service.

What do you think? I'm a little torn — I like a lot in this pull request, but it does change the API for anyone else who's using this. So from my perspective it doesn't seem wise to "break" what I consider a "legacy" API — but I'm open to whatever suggestions/feedback you've got.

(I really appreciate you taking the time to submit a pull request, and like I said I feel bad that the documentation does not make the state of this project more clear.)

Will White
Collaborator

Hi @natevw,
No worries! I've been using node-chargify for a bit now and actually didn't bother to do any searching around before sketching out this rewrite. Since the project is using semver, I don't think there would be a problem with breaking the API in a 0.2.x release. API users should not be anticipating a clean upgrade between these versions on the 0.x.x branch.

The plugin architecture of Fermata is interesting. I just prefer the Request module's API and I consider the approach on this branch almost as a "plugin" for the Request module.

I'm still interested in merging and supporting this branch, but I'm happy to discuss further.

Thanks,
Will

Nathan Vander Wilt
Owner
natevw commented

Thanks, Will.

I agree that Request is a great module as well — I've actually used it alongside Fermata in projects. To me they fill different roles: Request is great when you have a "static URL" (or dynamically rewritten, as in a proxy) and still want streaming/buffering, while I designed Fermata specifically for dealing with "answers" to/from REST API calls — I just got really sick of concat'ing URL strings together by hand all over my code (especially in clientside CouchApps) and so Fermata emphasizes that side of things. @mikeal's library emphasizes the crazy cool node.js async streaming stuff.

Anyway, if semver considers 0.2.x incompatible with 0.1.x this should be reasonably safe to merge in and I plan to do so once I've had a chance to properly read through the diff. Thanks again!

Mikeal Rogers
mikeal commented

Streams are great but they are only useful when you're taking data from one file descriptor and handing it over, and possibly mutating it, to another.

The standard node conventions for "asking question" from IO, of which REST API calls would certainly qualify, is the standard callback function (err, result) {} as the last argument. APIs that don't use this interface will have a very hard time integrating in to the rest of the node ecosystem and being using in conjunction with other libraries.

Since you have a higher level abstraction than request (REST > HTTP) you can consider a lot more as an "error", including HTTP errors, where as request only considers socket errors to be "errors".

Will White
Collaborator

I was torn on the decision to return certain HTTP codes (> 400, for instance) in err. For now I decided to just follow the Request API, but I'm happy to reconsider that. Good to hear your opinion on it. How would the API user be able to tell the difference between a HTTP level error vs. a socket error? In some cases that distinction is important to the user.

Mikeal Rogers
mikeal commented

It's all about where your layer is. I have a small CouchDB API I use on top of request and it's primary function is to turn non-2xx responses in to error :)

Nathan Vander Wilt natevw merged commit cc11ead into from
Nathan Vander Wilt natevw closed this
Nathan Vander Wilt
Owner
natevw commented

Merged in, thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
2  .gitignore
... ... @@ -1,2 +0,0 @@
1   -.DS_Store
2   -
108 README.md
Source Rendered
... ... @@ -1,56 +1,96 @@
1   -This is a fairly generic REST interface wrapper that allows Chargify URLs to be accessed from node.js.
2   -See <http://docs.chargify.com/api-resources> and surrounding pages for up-to-date Chargify documentation.
  1 +Easy integration with [Chargify][0] for adding recurring payments to your
  2 +application.
3 3
  4 +This module is essentially a wrapper around [Request][1], but adds a little
  5 +convenience for connecting to the [Chargify API][2].
4 6
5   -## Examples ##
  7 +[0]:http://chargify.com/
  8 +[1]:https://github.com/mikeal/request
  9 +[2]:http://docs.chargify.com/api-resources
  10 +[3]:https://github.com/mikeal/request/blob/master/README.md
6 11
7   -A site is wrapped in a representation of its base URL:
  12 +## Example
8 13
9   - var wrapped_site = chargify.wrapSite('example-site', "API_KEY");
  14 +You can normally require and instantiate at the same time using your Chargify
  15 +subdomain and API key.
10 16
11   -This base URL can be extended...
  17 + var chargify = require('chargify');
  18 + var chargify_site = chargify('YOUR-CHARGIFY-SUBDOMAIN', 'YOUR-API-KEY');
12 19
13   - var some_subscription = wrapped_site('subscriptions')(42);
  20 +List subscriptions:
14 21
15   -...and queried (i.e. GET):
  22 + chargify_site.get('subscriptions.json', function(err, res, body) {
  23 + if (err) throw err;
  24 + console.log(res.statusCode);
  25 + console.log(body);
  26 + });
16 27
17   - some_subscription(function (status, data) { if (status === 200) console.log(data.subscription.state); });
  28 +Load subscription #40:
18 29
19   -...and updated (i.e. PUT):
  30 + chargify_site.get('subscriptions/40.json', function(err, res, body) {
  31 + if (err) throw err;
  32 + console.log(res.statusCode);
  33 + console.log(body);
  34 + });
20 35
21   - some_subscription('components')(5)({component:{allocated_quantity:9}}, function (status, info) { console.log(info); });
  36 +Create a new customer:
  37 +
  38 + chargify_site.post({
  39 + pathname: 'customers.json',
  40 + json: {
  41 + customer: {
  42 + first_name: 'Joe',
  43 + last_name: 'Blow',
  44 + email: 'joe@example.com'
  45 + }
  46 + }
  47 + }, function(err, res, body) {
  48 + if (err) throw err;
  49 + console.log(res.statusCode);
  50 + console.log(body);
  51 + });
22 52
23   -Or, used to add or remove objects (i.e. POST, DELETE):
  53 +## Documentation
24 54
25   - var customerInfo = {customer:{first_name:"Sir",last_name:"Pedro",email:"user@example.com"}};
26   - wrapped_site('customers').add(customerInfo, function (s, info) {
27   - // NOTE: Chargify currently returns 403 from this request, as customer deletion is not supported
28   - wrapped_site('customers')(info.customer.id).remove(function (s, info) {});
29   - });
  55 +### chargify(subdomain, api_key)
  56 +
  57 +Returns a chargify_site. The available methods are listed below.
  58 +
  59 +- chargify_site.get(options, callback)
  60 +- chargify_site.post(options, callback)
  61 +- chargify_site.put(options, callback)
  62 +- chargify_site.del(options, callback)
  63 +
  64 +The first argument can be either a url or an options object. he only required
  65 +key is `uri`. The only required option is uri, all others are optional. The key
  66 +attributes are listed below. See the [Request module's README][3] for a full list.
  67 +
  68 +- `uri` - Required. The URI of the resource. `host`, `protocol`, and `auth`
  69 + information are optional.
  70 +- `json` - sets the body of the request using a JavaScript object.
  71 +
  72 +## See also
30 73
  74 +- [Request documentation](https://github.com/mikeal/request/blob/master/README.md)
  75 +- [Chargify API documentation][2]
31 76
32   -## Documentation ##
  77 +## Testing
33 78
34   -The module has but one function:
  79 +Before running the tests, you need to create a Chargify test site and specify
  80 +the site's subdomain and your API key in a JSON file called config.json.
35 81
36   -* `chargify.wrapSite(subdomain, key)` - return base URL wrapper for a Chargify site using the given API key.
  82 +Example config.json file:
37 83
38   -The actions available on this URL wrapper interface are as follows:
  84 + {
  85 + "chargifySubdomain": "chargify-test",
  86 + "chargifyAPIKey": "xxxxxxxxxxxxxx-9x_"
  87 + }
39 88
40   -* `()` - return wrapped URL as string
41   -* `(string/number/etc.)` - return another URL wrapper with given path component appended
42   -* `(callback)` - GET on wrapped URL [alias for `.get(null, cb)`]
43   -* `(dict, callback)` - PUT on wrapped URL [alias for `.put`]
44   -* `.add(dict, callback)` - POST on wrapped URL [alias for `.post`]
45   -* `.remove(callback)` - DELETE on wrapped URL [alias for `.del(null, cb)`]
46   -* `.get(dict, callback)` - GET on wrapped URL with query parameters from dict
47   -* `.put(dict, callback)` - PUT on wrapped URL with dict sent as JSON body
48   -* `.post(dict, callback)` - POST on wrapped URL with dict sent as JSON body
49   -* `.del(dict, callback)` - DELETE on wrapped URL with dict sent as JSON body
50   -* `.req(query, method, dict, callback)` - _METHOD_ on wrapped URL (plus query parameters) with data as JSON for body (`query` and `data` may be null)
  89 +Then simply run:
51 90
  91 + npm test
52 92
53   -## License ##
  93 +## License
54 94
55 95 Copyright © 2011 by &yet, LLC. Released under the terms of the MIT License:
56 96
@@ -70,4 +110,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
70 110 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
71 111 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
72 112 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
73   -THE SOFTWARE.
  113 +THE SOFTWARE.
195 chargify.js
... ... @@ -1,131 +1,68 @@
1   -/*
2   -// This is essentially a generic REST interface wrapper that allows Chargify's URLs to be formed and accessed.
3   -//
4   -// See http://docs.chargify.com/api-resources and surrounding pages for up-to-date Chargify documentation.
5   -//
6   -//
7   -// A site is wrapped in a representation of its base URL:
8   -// var wrapped_site = chargify.wrapSite('example-site', "API_KEY");
9   -//
10   -// This base URL can be extended...
11   -// var some_subscription = wrapped_site('subscriptions')(42);
12   -//
13   -// ...and queried (i.e. GET):
14   -// some_subscription(function (status, data) { if (status === 200) console.log(data.subscription.state); });
15   -//
16   -// ...and updated (i.e. PUT):
17   -// some_subscription('components')(5)({component:{allocated_quantity:9}}, function (s, info) { console.log(info); });
18   -//
19   -// ...and used to add or remove objects (i.e. POST, DELETE):
20   -// wrapped_site('customers').add({customer:{first_name:"Sir",last_name:"Pedro"}}, function (s, info) {
21   -// wrapped_site('customers')(info.customer.id).remove(function (s, info) {});
22   -// });
23   -//
24   -// The full API of the URL wrapper interface is as follows:
25   -// () - return wrapped URL as string
26   -// (string/number/etc) - return another URL wrapper with given path component appended
27   -// (callback) - GET on wrapped URL [alias for .get(null, cb)]
28   -// (dict, callback) - PUT on wrapped URL [alias for .put]
29   -// .add(dict, callback) - POST on wrapped URL [alias for .post]
30   -// .remove(callback) - DELETE on wrapped URL [alias for .del(null, cb)]
31   -// .get(dict, callback) - GET on wrapped URL with query parameters from dict
32   -// .put(dict, callback) - PUT on wrapped URL with dict sent as JSON body
33   -// .post(dict, callback) - POST on wrapped URL with dict sent as JSON body
34   -// .del(dict, callback) - DELETE on wrapped URL with dict sent as JSON body
35   -// .req(query, method, dict, callback) - <method> on wrapped URL (plus query parameters) with data as JSON for body (query, data may be null)
36   -//
37   -// Ideally it'd be an even cleaner (e.g.) `site.subscriptions[id].components[id].usages()` once JS gets Proxy object
38   -// support like Firefox now has -- see http://code.google.com/p/v8/issues/detail?id=633 for V8 implementation status.
39   -// It would be easy to make this pluggable for *any* JSON-compatible REST API as well.
40   -*/
41   -exports.wrapSite = function (subdomain, api_key) {
42   - var host = subdomain + ".chargify.com";
43   - var auth = 'Basic ' + new Buffer(api_key + ':x').toString('base64');
44   -
45   - var https = require('https');
46   - function _request(path, method, requestObj, yieldResponse) {
47   - var req = https.request({
48   - host: host,
49   - method: method,
50   - path: path,
51   - headers: {
52   - 'Authorization': auth,
53   - 'Content-Type': "application/json",
54   - 'Content-Length': 0,
55   - 'Accept': "application/json"
56   - }
57   - });
58   - if (requestObj) {
59   - var requestText = JSON.stringify(requestObj);
60   - req.setHeader('Content-Length', Buffer.byteLength(requestText))
61   - req.write(requestText);
62   - }
63   - req.end();
64   -
65   - req.on('error', function () {
66   - yieldResponse(0, "Connection error");
67   - });
68   - req.on('response', function (res) {
69   - res.setEncoding('utf8');
70   - var responseText = "";
71   - res.on('data', function (chunk) {
72   - responseText += chunk;
73   - });
74   - res.on('end', function () {
75   - try {
76   - var responseObj = JSON.parse(responseText);
77   - } catch (e) {
78   - return yieldResponse(res.statusCode, responseText);
79   - }
80   - yieldResponse(res.statusCode, responseObj);
81   - });
82   - });
83   - }
84   -
85   - function _makeExtendableURL(path) {
86   - path || (path = '');
87   - var extendableURL = function () {
88   - if (arguments.length === 0) {
89   - return "https://" + host + path;
90   - } else if (arguments.length === 1 && typeof(arguments[0]) === 'function') {
91   - extendableURL.get(null, arguments[0]);
92   - } else if (arguments.length === 1) {
93   - return _makeExtendableURL(path + '/' + encodeURIComponent(arguments[0]));
94   - } else if (arguments.length === 2) {
95   - extendableURL.put(arguments[0], arguments[1]);
96   - }
97   - };
98   -
99   - extendableURL.req = function (query, method, data, callback) {
100   - var pathWithQuery = (path) ? path + '.json' : '/';
101   - if (query) {
102   - pathWithQuery += '?' + Object.keys(query).map(function (key) {
103   - return encodeURIComponent(key) + '=' + encodeURIComponent(query[key]);
104   - }).join('&');
105   - }
106   - _request(pathWithQuery, method, data, callback);
107   - };
108   -
109   - extendableURL.get = function (query, callback) {
110   - extendableURL.req(query, 'GET', null, callback);
111   - };
112   -
113   - extendableURL.put = function (data, callback) {
114   - extendableURL.req(null, 'PUT', data, callback);
115   - };
116   -
117   - extendableURL.add = extendableURL.post = function (data, callback) {
118   - extendableURL.req(null, 'POST', data, callback);
119   - };
120   -
121   - extendableURL.remove = function (callback) {
122   - extendableURL.req(null, 'DELETE', null, callback);
123   - };
124   - extendableURL.del = function (data, callback) {
125   - extendableURL.req(null, 'DELETE', data, callback);
  1 +var request = require('request');
  2 +var _ = require('underscore');
  3 +var url = require('url');
  4 +
  5 +function Chargify(options) {
  6 + if (typeof options === 'string') {
  7 + options = {
  8 + subdomain: arguments[0],
  9 + api_key: arguments[1]
126 10 };
127   -
128   - return extendableURL;
  11 + };
  12 + if (!(this instanceof Chargify)) {
  13 + return new Chargify(options);
129 14 }
130   - return _makeExtendableURL();
131   -}
  15 + this.host = options.subdomain + '.chargify.com';
  16 + this.api_key = options.api_key;
  17 +};
  18 +
  19 +Chargify.prototype.request = function(options, callback) {
  20 + options.uri = options.uri || options.url;
  21 + options.uri = url.format(_(url.parse(options.uri)).defaults({
  22 + protocol: 'https',
  23 + host: this.host,
  24 + auth: this.api_key + ':x'
  25 + }));
  26 + options.headers = {
  27 + 'accept': 'application/json'
  28 + };
  29 + request(options, function(err, res, body) {
  30 + if (err) return callback(err);
  31 + try {
  32 + var body = JSON.parse(body);
  33 + } catch(e) {}
  34 + callback(err, res, body);
  35 + });
  36 +};
  37 +
  38 +Chargify.prototype.get = function(options, callback) {
  39 + if (typeof options === 'string') options = {uri:options}
  40 + options.method = 'GET';
  41 + this.request(options, callback);
  42 +};
  43 +
  44 +Chargify.prototype.post = function(options, callback) {
  45 + if (typeof options === 'string') options = {uri:options}
  46 + options.method = 'POST';
  47 + this.request(options, callback);
  48 +};
  49 +
  50 +Chargify.prototype.put = function(options, callback) {
  51 + if (typeof options === 'string') options = {uri:options}
  52 + options.method = 'PUT';
  53 + this.request(options, callback);
  54 +};
  55 +
  56 +Chargify.prototype.head = function(options, callback) {
  57 + if (typeof options === 'string') options = {uri:options}
  58 + options.method = 'HEAD';
  59 + this.request(options, callback);
  60 +};
  61 +
  62 +Chargify.prototype.del = function(options, callback) {
  63 + if (typeof options === 'string') options = {uri:options}
  64 + options.method = 'DELETE';
  65 + this.request(options, callback);
  66 +};
  67 +
  68 +module.exports = Chargify;
48 package.json
... ... @@ -1,11 +1,45 @@
1 1 {
2 2 "name": "chargify",
  3 + "description": "Easy integration with Chargify for adding recurring payments to your application.",
3 4 "version": "0.1.1",
4 5 "homepage": "https://github.com/andyet/node-chargify",
5   - "repository":{"type":"git", "url":"https://github.com/andyet/node-chargify.git"},
6   - "author": {"name":"&yet, LLC", "url":"http://andyet.net"},
7   - "contributors": [{"name":"Nathan Vander Wilt", "email": "nate@andyet.net", "url":"http://cloudcartography.com"}],
8   - "description": "A fairly generic REST interface wrapper for building and accessing Chargify API URLs.",
9   - "licenses": [{"type": "MIT", "url": "https://github.com/andyet/node-chargify/raw/master/README.md"}],
10   - "main": "chargify"
11   -}
  6 + "repository": {
  7 + "type": "git",
  8 + "url": "https://github.com/andyet/node-chargify.git"
  9 + },
  10 + "author": {
  11 + "name": "&yet, LLC",
  12 + "url": "http://andyet.net"
  13 + },
  14 + "contributors": [
  15 + {
  16 + "name": "Nathan Vander Wilt",
  17 + "email": "nate@andyet.net",
  18 + "url": "http://cloudcartography.com"
  19 + },
  20 + {
  21 + "name": "Will White",
  22 + "email": "will@mapbox.com",
  23 + "url": "http://mapbox.com"
  24 + }
  25 + ],
  26 + "licenses": [
  27 + {
  28 + "type": "MIT"
  29 + }
  30 + ],
  31 + "main": "chargify",
  32 + "dependencies": {
  33 + "request": "2.2.x",
  34 + "underscore": "1.1.x"
  35 + },
  36 + "engines": {
  37 + "node": ">= 0.6.x"
  38 + },
  39 + "devDependencies": {
  40 + "mocha": "1.0.x"
  41 + },
  42 + "scripts": {
  43 + "test": "mocha"
  44 + }
  45 +}
142 test/chargify.js
... ... @@ -0,0 +1,142 @@
  1 +var assert = require('assert');
  2 +var path = require('path');
  3 +var fs = require('fs');
  4 +var chargify = require('../chargify');
  5 +
  6 +describe('chargify', function() {
  7 + before(function(done) {
  8 + if (path.existsSync('config.json')) {
  9 + var config = JSON.parse(fs.readFileSync('config.json'));
  10 + chargify = chargify(config.chargifySubdomain, config.chargifyAPIKey);
  11 + } else {
  12 + console.error('config.json is required to run tests');
  13 + process.exit(1);
  14 + }
  15 + done();
  16 + });
  17 + describe('customer', function() {
  18 + var id;
  19 + describe('post()', function() {
  20 + it('should return HTTP 201 for a valid customer', function(done) {
  21 + chargify.post({
  22 + uri: 'customers.json',
  23 + json: {
  24 + customer: {
  25 + first_name: 'Joe',
  26 + last_name: 'Blow',
  27 + email: 'joe@example.com'
  28 + }
  29 + }
  30 + }, function(err, res, body) {
  31 + if (err) throw err;
  32 + assert.equal(res.statusCode, 201);
  33 + assert.ok(body.customer);
  34 + assert.ok(body.customer.id);
  35 + assert.equal(body.customer.first_name, 'Joe');
  36 + id = body.customer.id;
  37 + done();
  38 + });
  39 + });
  40 + it('should return HTTP 422 when last_name is missing', function(done) {
  41 + chargify.post({
  42 + uri: 'customers.json',
  43 + json: {
  44 + customer: {
  45 + first_name: 'Joe',
  46 + email: 'joe@example.com'
  47 + }
  48 + }
  49 + }, function(err, res, body) {
  50 + if (err) throw err;
  51 + assert.equal(res.statusCode, 422);
  52 + assert.ok(body.errors);
  53 + assert.equal(body.errors[0], 'Last name: cannot be blank.');
  54 + done();
  55 + });
  56 + });
  57 + });
  58 + describe('put()', function() {
  59 + it('should return HTTP 200 for a valid customer', function(done) {
  60 + chargify.put({
  61 + uri: 'customers/' + id + '.json',
  62 + json: {
  63 + customer: {
  64 + last_name: 'Smith'
  65 + }
  66 + }
  67 + }, function(err, res, body) {
  68 + if (err) throw err;
  69 + assert.equal(res.statusCode, 200);
  70 + assert.ok(body.customer);
  71 + assert.ok(body.customer.id);
  72 + assert.equal(body.customer.first_name, 'Joe');
  73 + assert.equal(body.customer.last_name, 'Smith');
  74 + done();
  75 + });
  76 + });
  77 + it('should return HTTP 422 for an invalid email', function(done) {
  78 + chargify.put({
  79 + uri: 'customers/' + id + '.json',
  80 + json: {
  81 + customer: {
  82 + email: 'bademail'
  83 + }
  84 + }
  85 + }, function(err, res, body) {
  86 + if (err) throw err;
  87 + assert.equal(res.statusCode, 422);
  88 + assert.ok(body.errors);
  89 + assert.equal(body.errors[0], 'Email address: must be a valid email format.');
  90 + done();
  91 + });
  92 + });
  93 + });
  94 + describe('get()', function() {
  95 + it('should list users', function(done) {
  96 + chargify.get('customers.json', function(err, res, body) {
  97 + if (err) throw err;
  98 + assert.equal(res.statusCode, 200);
  99 + assert.ok(body.length);
  100 + done();
  101 + });
  102 + });
  103 + it('should list users with ?page=2', function(done) {
  104 + chargify.get('customers.json?page=2', function(err, res, body) {
  105 + if (err) throw err;
  106 + assert.equal(res.statusCode, 200);
  107 + done();
  108 + });
  109 + });
  110 + it('should return HTTP 200 for an existing user', function(done) {
  111 + chargify.get('customers/' + id + '.json', function(err, res, body) {
  112 + if (err) throw err;
  113 + assert.equal(res.statusCode, 200);
  114 + assert.ok(body.customer);
  115 + assert.ok(body.customer.id);
  116 + assert.equal(body.customer.first_name, 'Joe');
  117 + assert.equal(body.customer.last_name, 'Smith');
  118 + done();
  119 + });
  120 + });
  121 + it('should return HTTP 404 for an unknown user', function(done) {
  122 + chargify.get('customers/99999999.json', function(err, res, body) {
  123 + if (err) throw err;
  124 + assert.equal(res.statusCode, 404);
  125 + done();
  126 + });
  127 + });
  128 + });
  129 + describe('del()', function() {
  130 + it('should return HTTP 201 for a valid customer', function(done) {
  131 + chargify.del({
  132 + uri: 'customers/' + id + '.json',
  133 + }, function(err, res, body) {
  134 + if (err) throw err;
  135 + assert.equal(res.statusCode, 403);
  136 + assert.equal(body, '403 Forbidden');
  137 + done();
  138 + });
  139 + });
  140 + });
  141 + });
  142 +});

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.