diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..1f2497f --- /dev/null +++ b/.jscsrc @@ -0,0 +1,18 @@ +{ + "preset": "airbnb", + "disallowMultipleVarDecl": { + "allExcept": ["require"] + }, + "maxErrors": "Infinity", + "requireTrailingComma": null, + "requirePaddingNewLinesAfterBlocks": null, + "requirePaddingNewLinesBeforeLineComments": null, + "requireSpacesInsideObjectBrackets": "all", + "disallowSpacesInFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "requireSpacesInAnonymousFunctionExpression": null +} diff --git a/.jshintrc b/.jshintrc index 3cbe1ad..ed5597a 100644 --- a/.jshintrc +++ b/.jshintrc @@ -68,6 +68,7 @@ "alert": false, "alertify": false, "printStackTrace": false, - "Spinner": false + "Spinner": false, + "Headers": false } } diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3941d22 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: node_js +node_js: +- '6' +- '5' +- '4' +deploy: + provider: npm + api_key: + secure: Fj94zdVEW7vdnA2lQ2J2u5Jgt49XG9t5vTWf6/wHrhC85b1p+QGCNbfYg7+NkiCK7ChDealidty2ZPEcjdAlBErYbg29caKT6iNgVJ+sebm5ECy/V8Vfy6bqD8M6eI5kcOR2Iwwn22bt9P2kown/O3uZvvGd58eUpDBsf/Ru5DsoMt4/gNqQLZdAE+GWVqPVA4R7hL1ehCn2SMTni0xhIEoKbG0AWWjU+67qeLrFmQdrVvlsaHlXZO5E8UgEFjqkiF8EpqPJaGZf5ivLgS84NS9RrRVuKn0lmb/YLqzx+d1BWbaTmCLioWMU31Z61uqrB1PwVIrZygMIsbwqjZ43aOnl/H43TkUAwXVzCcgLvPN9WLMMEudX3XiyfsyO/3ddTv5tpkZehDLHQxVIIPNCwLOy6hR9SVfd9SNF2QXZ2kblebATL58/jG5lQQDw8n3OUXKlFeoAVN+nsbNOv0aiV6VyDqqGUfn28UxTlJ0Cgs0o6SjhSoXMSRjLyHSJtbcOPh9hmU31prKOHKsuyz1Z4dIDfQ+XM+Q5E4j0saJWALyv2N7jOpiZaLGMFKA+j8GSGQMENGSRwXQQTO6Kb4iNwTtChqAwWmop9h1kAvGA+BJje+oUMiyjJO7yuGudNCwsovlbJcTsvU2m4o4YwV+xlzRlUZqEnJ01ccVJUnGraN4= + on: + tags: true + repo: Receiptful/node-woocommerce + email: + secure: FNuDs+J0+eAbQtnybfHroIWmnvt0SK5VW+yI31O1GTgidHCwm873Ea41tJiZp8x4MpwVgZmhxB0P4QJjVE+/QK/Ac+GRbMbm3/lcq6exqgMmInelQxRMbKejyc1xEMeGiH2nr87vxhB/PTM2ERxsLFd+sGDS3bcrjMiY82WWCxwMGE32e9zD5D4ce7PGKNn5Qjev4flcwTKwSUBvtXFXj+G/t3xkB6I5c76CkOiqI2130wpx1CuAtDvg56cteI4MEyi4L+36s6Cvj/djIuUWn0I+9Ze68A3QOuj4+Gw/KYR+KJJZN8V0gMy58sfQm4M4EtHccVeVVRN/BXnMl+a75mKn9Gg8NE8NivWENiI/0iU24+WufaX7+klFwnItZsY7TmCmCIO68wzqPhG50a3r8hbnXqdn/CdjGiV3OkZe744Rrg/+ZXrcIolyRF3VFEf05iD4YNhgU11YU7WG4Qgm3sptGGiuBJMaWU7vejE+Z3n3lKF2HN/SOsC48XJWUXtrdpYa+xgZnaOA+1V7ccfqjFerE9ngbml/wfZ9KIKApyDnZ5jlaJO5FpaIWaSYTvUHoc/sPXUwLSYQFWYzWm6FhbvkIa3wQ7xdZxvoi70+e5iqsuW+LV9CzoVNFOBgvpPAj7t2cev2jgS+b7f3Z4ivG02hse/Hyc0U8jWDxyTPvkc= +notifications: + slack: + secure: oHvm9eOny5Pg3NzQash8U75BWMZ9oT9liM2LoOmTZTpKa04nKhkwQSYdpG7VkRK9Txfvh0zg2F2lC35QrbiP7T3IqkzRF5ktqxLnO8GhNhXd754vZ3b6k093hP9l8OG8tEKqkEi7uceV+1qkmgaNTbY8qk0641oBvqM1J1cXuQTmdtixl1+fXCkybfiVNRmDoAEEk0vDB+/MVLPvUSQigvZmI85pLmGlfZWbJ1PG/lIcU3tJ48ys4T18P4C97xN8KZIxcakmbWFzcB8iS/9t3HVyuS9CvSFqab3gRVc1TIcCwIApgGxtAFQtDerz4haWnodjh18+KGwRxRUqLP3erw1s1NSbhgQzC+kfV0IL14tdrvPuA0f0edwAv4qA9s9eyjgmo/Q5BdWx5TWTNpKC4Umj6IzE5YrCT0aHxqMTT1KTqw4e4Lv+Ml6ScZhi34vhe4zo9QF0oCmRJ+8a203SgODni4l2rmW1FsYd+kGCKD9e8CBjI7kr5cPkPr6h1wgsPH6x7iW4SxWOymABdqh6HPCnMHK63fRo7wvZfw50Hrtv9ieCc64tx2TIYdxjMBHlvbxOCi7WkJSH5VXq7XAK+7zrVSuvyO27DY6DPhPS8A4YGEcOmoWfmeanTGUkpjcciyN3w+TfSfDbr7K3LCWAG5r7SynUWEWJqZW3N8XeE1k= diff --git a/Makefile b/Makefile deleted file mode 100644 index 14a7255..0000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -test: - ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha - -.PHONY: test diff --git a/README.md b/README.md index a251061..330dd7e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # node-woocommerce Connects NodeJS to the glorious world of the WooCommerce API -[![Code Climate](https://codeclimate.com/repos/5551dd2f6956804225000037/badges/4935563d1fc24b707863/gpa.svg)](https://codeclimate.com/repos/5551dd2f6956804225000037/feed) +[![Build Status](https://travis-ci.org/Receiptful/node-woocommerce.svg?branch=master)](https://travis-ci.org/Receiptful/node-woocommerce) ## Important v2.0 Changes @@ -18,10 +18,10 @@ wooCommerce.get('/products') .catch(err => { // Log the error message console.log(err.message); - + // Log the body returned from the server console.log(err.body); - + // Log the full response object and status code console.log(err.response, err.response.statusCode); }); diff --git a/lib/logger.js b/lib/logger.js index e34d6bb..00b6375 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -11,7 +11,5 @@ module.exports = { if (this.logLevel < 1) return; console.log('\x1b[36mInfo: \x1b[0m%s', message); }, - error: function(message) { - console.log('\x1b[31mError: \x1b[0m%s', message); - } -} + error: message => console.error('\x1b[31mError: \x1b[0m%s', message) +}; diff --git a/lib/request.js b/lib/request.js index da03560..0224c9a 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,7 +1,4 @@ 'use strict'; -/** - * To make the requests asynchronously - */ const logger = require('./logger'), oAuth = require('oauth-1.0a'), @@ -9,6 +6,7 @@ const logger = require('./logger'), const version = require('../package.json').version; +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers class Request { constructor(options) { @@ -24,9 +22,9 @@ class Request { public: this.options.consumerKey, secret: this.options.secret }, - 'signature_method': 'HMAC-SHA256', + signature_method: 'HMAC-SHA256', version: null, - 'last_ampersand': false + last_ampersand: false }); } @@ -64,8 +62,9 @@ class Request { qs: {}, headers: { 'User-Agent': `node-woocommerce/${version}`, - 'Accept': 'application/json, *.*' - } + Accept: 'application/json, *.*' + }, + timeout: this.options.timeout }; if (data) { @@ -148,10 +147,8 @@ class Request { } }); - }); } - } module.exports = Request; diff --git a/lib/woocommerce.js b/lib/woocommerce.js index ac0d097..b43ac42 100644 --- a/lib/woocommerce.js +++ b/lib/woocommerce.js @@ -45,7 +45,8 @@ class WooCommerce { port: this.options.port, consumerKey: this.options.consumerKey, secret: this.options.secret, - logLevel: this.options.logLevel + logLevel: this.options.logLevel, + timeout: this.options.timeout }); logger.info(require('util').inspect(this.options)); @@ -76,5 +77,4 @@ class WooCommerce { } } - module.exports = WooCommerce; diff --git a/package.json b/package.json index 37973b9..d6cdcbd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,11 @@ "description": "Connects NodeJS to the glorious world of the WooCommerce API", "main": "lib/woocommerce.js", "scripts": { - "test": "make test" + "exectests": "node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha", + "jshint": "node_modules/.bin/jshint lib test", + "jscs": "node_modules/.bin/jscs lib test", + "lint": "npm run jshint && npm run jscs", + "test": "npm run exectests && npm run lint" }, "repository": { "type": "git", @@ -27,6 +31,8 @@ "devDependencies": { "chai": "^3.5.0", "istanbul": "^0.4.2", + "jscs": "^2.9.0", + "jshint": "^2.9.1", "mocha": "^2.4.5", "nock": "^7.2.2", "sinon": "^1.17.3" diff --git a/test/logger.js b/test/logger.js new file mode 100644 index 0000000..ef33091 --- /dev/null +++ b/test/logger.js @@ -0,0 +1,40 @@ +'use strict'; + +const logger = require('../lib/logger'), + sinon = require('sinon'); + +describe('logger', () => { + const self = { }; + + beforeEach(() => self.sandbox = sinon.sandbox.create()); + afterEach(() => self.sandbox.restore()); + + it('Should be set to a log level of zero by default', () => { + logger.logLevel.should.equal(0); + }); + + it('Should log an error at any log level', () => { + const spy = self.sandbox.spy(console, 'error'); + logger.error('Logging test error at 0'); + logger.level = 1; + logger.error('Logging test error at 1'); + + sinon.assert.calledTwice(spy); + }); + + it('Should log info at level one', () => { + var spy = self.sandbox.spy(console, 'log'); + logger.logLevel = 1; + logger.info('Logging test error at 1'); + + sinon.assert.calledOnce(spy); + }); + + it('Should not log info at level zero', () => { + const spy = self.sandbox.spy(console, 'log'); + logger.logLevel = 0; + logger.info('Logging test error at 1'); + + sinon.assert.notCalled(spy); + }); +}); diff --git a/test/request.js b/test/request.js new file mode 100644 index 0000000..d11c5cb --- /dev/null +++ b/test/request.js @@ -0,0 +1,168 @@ +'use strict'; + +const Request = require('../lib/request'), + chai = require('chai'), + nock = require('nock'); + +const should = chai.should(); + +describe('Request', () => { + beforeEach(() => nock.cleanAll()); + + const rOAuth = new Request({ + hostname: 'http://foo.com', + consumerKey: 'foo', + secret: 'foo', + headers: { + test: 'header' + } + }); + + const rBasic = new Request({ + hostname: 'https://foo.com', + ssl: true, + port: 443, + consumerKey: 'foo', + secret: 'foo', + headers: { + test: 'header' + } + }); + + it('Should return an error if hostname is missing', () => { + should.Throw(() => { + new Request(); + }, Error); + }); + + it('Should return an error on bad request', done => { + const api = nock('http://foo.com') + .filteringPath(/\?.*/g, '?xxx') + .post('/orders?xxx', {}) + .reply(400, { success: true }); + + rOAuth.complete('post', '/orders', {}, err => { + err.should.be.an.instanceof(Error); + api.done(); + done(); + }); + }); + + it('Should return an error on internal server error', done => { + const api = nock('http://foo.com') + .filteringPath(/\?.*/g, '?xxx') + .post('/orders?xxx', {}) + .reply(500, { success: true }); + + rOAuth.complete('post', '/orders', {}, err => { + err.should.be.an.instanceof(Error); + api.done(); + done(); + }); + }); + + it('Should return an error the request JSON is malformed', done => { + const api = nock('http://foo.com') + .defaultReplyHeaders({ + 'content-type': 'application/json' + }) + .filteringPath(/\?.*/g, '?xxx') + .post('/orders?xxx', {}) + .reply(200, ''); + + rOAuth.complete('post', '/orders', {}, err => { + err.should.be.an.instanceof(Error); + err.message.should.match(/Unexpected token { + const api = nock('http://foo.com') + .filteringPath(/\?.*/g, '?xxx') + .post('/orders?xxx', {}) + .reply(200, '{ "success": true }'); + + rOAuth.complete('post', '/orders', {}, (err, data) => { + should.not.exist(err); + data.should.be.an.instanceof(Object) + .and.have.property('success', true); + api.done(); + done(); + }); + }); + + it('Should support data for GET requests', done => { + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers + const api = nock('https://foo.com/') + .get('/orders') + .query({ + consumer_key: 'foo', + consumer_secret: 'foo', + filter: { + limit: 10 + } + }) + .reply(200, '{ "success": true }', { + 'content-type': 'application/json' + }); + + rBasic.complete('get', '/orders', { 'filter[limit]': 10 }, (err, data) => { + should.not.exist(err); + data.should.be.an.instanceof(Object) + .and.have.property('success', true); + api.done(); + done(); + }); + }); + + it('Should return content for https using Basic Auth', done => { + const api = nock('https://foo.com/') + .post('/orders') + .query(true) + .reply(200, '{ "success": true }'); + + rBasic.complete('post', '/orders', {}, (err, data) => { + should.not.exist(err); + data.should.be.an.instanceof(Object) + .and.have.property('success', true); + api.done(); + done(); + }); + }); + + it('Should return content for when not a json', done => { + const api = nock('http://foo.com') + .defaultReplyHeaders({ + 'content-type': 'application/xml' + }) + .filteringPath(/\?.*/g, '?xxx') + .post('/orders?xxx', {}) + .reply(200, ''); + + rOAuth.complete('post', '/orders', {}, (err, data) => { + api.done(); + should.not.exist(err); + data.should.equal(''); + api.done(); + done(); + }); + }); + + it('Should return an error if "errors" are found in the response', done => { + const api = nock('https://foo.com') + .filteringPath(/\?.*/g, '?xxx') + .get('/errors?xxx') + .reply(200, '{ "errors": ["An error has occurred."] }', { + 'content-type': 'application/json' + }); + + rBasic.complete('get', '/errors', {}, err => { + api.done(); + err.should.be.an.instanceof(Error); + err.message.should.equal('["An error has occurred."]'); + done(); + }); + }); +}); diff --git a/test/woocommerce.js b/test/woocommerce.js index c301829..c5ff7e0 100644 --- a/test/woocommerce.js +++ b/test/woocommerce.js @@ -1,324 +1,103 @@ 'use strict'; -var WooCommerce = require('../lib/woocommerce'), - logger = require('../lib/logger'), +const WooCommerce = require('../lib/woocommerce'), Request = require('../lib/request'), - should = require('chai').should(), - nock = require('nock'), + chai = require('chai'), sinon = require('sinon'); +const should = chai.should(); -describe('Constructor: #WooCommerce', () => { +describe('WooCommerce', () => { + const self = { }; - it('Should throw an error if the consumerKey or secret are missing', () => { + beforeEach(() => self.sandbox = sinon.sandbox.create()); + afterEach(() => self.sandbox.restore()); + + describe('Constructor: #WooCommerce', () => { + it('Should throw an error if the consumerKey or secret are missing', () => { should.Throw(() => { new WooCommerce(); }, Error); }); - it('Should throw an error if the url is missing', () => { - should.Throw(() => { - new WooCommerce({ + it('Should throw an error if the url is missing', () => { + should.Throw(() => { + new WooCommerce({ + consumerKey: 'foo', + secret: 'foo' + }); + }, Error); + }); + + it('Should set the correct default when requirements are met', () => { + const wc = new WooCommerce({ + url: 'foo.com', consumerKey: 'foo', secret: 'foo' }); - }, Error); - }); - it('Should set the correct default when requirements are met', () => { - var wc = new WooCommerce({ - url: 'foo.com', - consumerKey: 'foo', - secret: 'foo' + wc.options.logLevel.should.equal(0); + wc.options.ssl.should.equal(false); + wc.options.apiPath.should.equal('/wc-api/v2'); }); - wc.options.logLevel.should.equal(0); - wc.options.ssl.should.be.false; - wc.options.apiPath.should.equal('/wc-api/v2'); - }); + it('Should set the correct ssl default for https', () => { + const wc = new WooCommerce({ + url: 'https://foo.com', + consumerKey: 'foo', + secret: 'foo' + }); - it('Should set the correct ssl default for https', () => { - var wc = new WooCommerce({ - url: 'https://foo.com', - consumerKey: 'foo', - secret: 'foo' + wc.options.ssl.should.equal(true); }); - wc.options.ssl.should.be.true; + it('Should set the correct ssl default for http', () => { + const wc = new WooCommerce({ + url: 'http://foo.com', + consumerKey: 'foo', + secret: 'foo' + }); + + wc.options.ssl.should.equal(false); + }); }); - it('Should set the correct ssl default for http', () => { - var wc = new WooCommerce({ - url: 'http://foo.com', + describe('Helper Methods: #WooCommerce', () => { + const wc = new WooCommerce({ + url: 'foo.com', consumerKey: 'foo', secret: 'foo' }); - wc.options.ssl.should.be.false; - }); - -}); - -describe('Helper Methods: #WooCommerce', () => { - - var wc = new WooCommerce({ - url: 'foo.com', - consumerKey: 'foo', - secret: 'foo' - }); - - describe('Get', () => { - it('Should call the request', done => { - var requestMock = sinon.mock(Request.prototype); - var expectation = requestMock - .expects('complete') - .once() + beforeEach(() => { + self.completeStub = self.sandbox.stub(Request.prototype, 'complete') .yields(); - - wc.get('/foo', () => { - expectation.verify(); - requestMock.restore(); - done(); - }); }); - }); - describe('Post', () => { - it('Should call the request', done => { - var requestMock = sinon.mock(Request.prototype); - var expectation = requestMock - .expects('complete') - .once() - .yields(); + afterEach(() => sinon.assert.calledOnce(self.completeStub)); - wc.post('/foo', {}, () => { - expectation.verify(); - requestMock.restore(); - done(); + describe('Get', () => { + it('Should call the request', done => { + wc.get('/foo', done); }); }); - }); - - describe('Put', () => { - it('Should call the request', done => { - var requestMock = sinon.mock(Request.prototype); - var expectation = requestMock - .expects('complete') - .once() - .yields(); - wc.put('/foo', {}, () => { - expectation.verify(); - requestMock.restore(); - done(); + describe('Post', () => { + it('Should call the request', done => { + wc.post('/foo', {}, done); }); }); - }); - - describe('Delete', () => { - it('Should call the request', done => { - var requestMock = sinon.mock(Request.prototype); - var expectation = requestMock - .expects('complete') - .once() - .yields(); - wc.delete('/foo', () => { - expectation.verify(); - requestMock.restore(); - done(); + describe('Put', () => { + it('Should call the request', done => { + wc.put('/foo', {}, done); }); }); - }); - -}); - -describe('Logger: #WooCommerce', () => { - it('Should be set to a log level of zero by default', () => { - logger.logLevel.should.equal(0); - }); - - it('Should log an error at any log level', () => { - var spy = sinon.spy(console, 'log'); - logger.error('Logging test error at 0'); - logger.level = 1; - logger.error('Logging test error at 1'); - spy.calledTwice.should.be.true; - spy.restore(); - }); - it('Should log info at level one', () => { - var spy = sinon.spy(console, 'log'); - logger.logLevel = 1; - logger.info('Logging test error at 1'); - spy.calledOnce.should.be.true; - spy.restore(); - }); - - it('Should not log info at level zero', () => { - const spy = sinon.spy(console, 'log'); - logger.logLevel = 0; - logger.info('Logging test error at 1'); - spy.callCount.should.equal(0); - spy.restore(); - }); -}); - -describe('Request: #WooCommerce', () => { - - beforeEach(() => { - nock.cleanAll(); - }); - - const rOAuth = new Request({ - hostname: 'http://foo.com', - consumerKey: 'foo', - secret: 'foo', - headers: { - test: 'header' - } - }); - - const rBasic = new Request({ - hostname: 'https://foo.com', - ssl: true, - port: 443, - consumerKey: 'foo', - secret: 'foo', - headers: { - test: 'header' - } - }); - - it('Should return an error if hostname is missing', () => { - should.Throw(() => { - new Request(); - }, Error); - }); - - it('Should return an error on bad request', done => { - const api = nock('http://foo.com') - .filteringPath(/\?.*/g, '?xxx') - .post('/orders?xxx', {}) - .reply(400, { success: true }); - - rOAuth.complete('post', '/orders', {}, err => { - err.should.not.be.null; - api.done(); - done(); - }); - }); - - it('Should return an error on internal server error', done => { - const api = nock('http://foo.com') - .filteringPath(/\?.*/g, '?xxx') - .post('/orders?xxx', {}) - .reply(500, { success: true }); - - rOAuth.complete('post', '/orders', {}, err => { - err.should.not.be.null; - api.done(); - done(); - }); - }); - - it('Should return an error the request JSON is malformed', done => { - const api = nock('http://foo.com') - .defaultReplyHeaders({ - 'content-type': 'application/json' - }) - .filteringPath(/\?.*/g, '?xxx') - .post('/orders?xxx', {}) - .reply(200, ''); - - rOAuth.complete('post', '/orders', {}, err => { - err.should.not.be.null; - err.message.should.equal('Unexpected token <'); - api.done(); - done(); - }); - }); - - it('Should return content for http using OAuth', done => { - const api = nock('http://foo.com') - .filteringPath(/\?.*/g, '?xxx') - .post('/orders?xxx', {}) - .reply(200, '{ "success": true }'); - - rOAuth.complete('post', '/orders', {}, (err, data) => { - should.not.exist(err); - data.should.be.a.string; - api.done(); - done(); - }); - }); - - it('Should support data for GET requests', done => { - const api = nock('https://foo.com/') - .get('/orders') - .query({ - consumer_key: 'foo', - consumer_secret: 'foo', - filter: { - limit: 10 - } - }) - .reply(200, '{ "success": true }'); - - rBasic.complete('get', '/orders', { 'filter[limit]': 10 }, (err, data) => { - should.not.exist(err); - data.should.be.a.string; - api.done(); - done(); - }); - }); - - it('Should return content for https using Basic Auth', done => { - const api = nock('https://foo.com/') - .post('/orders') - .query(true) - .reply(200, '{ "success": true }'); - - rBasic.complete('post', '/orders', {}, (err, data) => { - should.not.exist(err); - data.should.be.a.string; - api.done(); - done(); - }); - }); - - it('Should return content for when not a json', done => { - const api = nock('http://foo.com') - .defaultReplyHeaders({ - 'content-type': 'application/xml' - }) - .filteringPath(/\?.*/g, '?xxx') - .post('/orders?xxx', {}) - .reply(200, ''); - - rOAuth.complete('post', '/orders', {}, (err, data) => { - api.done(); - should.not.exist(err); - data.should.be.a.string; - api.done(); - done(); - }); - }); - - it('Should return an error if "errors" are found in the response JSON', done => { - const api = nock('https://foo.com') - .filteringPath(/\?.*/g, '?xxx') - .get('/errors?xxx') - .reply(200, '{ "errors": ["An error has occurred."] }', { - 'content-type': 'application/json' + describe('Delete', () => { + it('Should call the request', done => { + wc.delete('/foo', done); }); - - rBasic.complete('get', '/errors', {}, err => { - api.done(); - err.should.not.be.null; - err.message.should.equal('["An error has occurred."]'); - done(); }); }); - });